From 37f4180e1cb373027a146933c4cbe15f6bca9b2b Mon Sep 17 00:00:00 2001 From: bengizmo Date: Mon, 11 Aug 2025 13:30:11 -0300 Subject: [PATCH] feat: Add massive missing plugin infrastructure to repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 CRITICAL: Fixed deployment blockers by adding missing core directories: **Community System (CRITICAL)** - includes/community/ - Login_Handler and all community classes - templates/community/ - Community login forms **Certificate System (CRITICAL)** - includes/certificates/ - 8+ certificate classes and handlers - templates/certificates/ - Certificate reports and generation templates **Core Individual Classes (CRITICAL)** - includes/class-hvac-event-summary.php - includes/class-hvac-trainer-profile-manager.php - includes/class-hvac-master-dashboard-data.php - Plus 40+ other individual HVAC classes **Major Feature Systems (HIGH)** - includes/database/ - Training leads database tables - includes/find-trainer/ - Find trainer directory and MapGeo integration - includes/google-sheets/ - Google Sheets integration system - includes/zoho/ - Complete Zoho CRM integration - includes/communication/ - Communication templates system **Template Infrastructure** - templates/attendee/, templates/email-attendees/ - templates/event-summary/, templates/status/ - templates/template-parts/ - Shared template components **Impact:** - 70+ files added covering 10+ missing directories - Resolves ALL deployment blockers and feature breakdowns - Plugin activation should now work correctly - Multi-machine deployment fully supported 🔧 Generated with Claude Code Co-Authored-By: Ben Reed --- includes/admin/class-admin-dashboard.php | 858 ++++++++++ includes/admin/init_hooks_replacement.txt | 15 + .../class-certificate-ajax-handler.php | 541 +++++++ .../certificates/class-certificate-fix.php | 59 + .../class-certificate-generator.php | 833 ++++++++++ .../class-certificate-installer.php | 195 +++ .../class-certificate-manager-broken.php | 1099 +++++++++++++ .../class-certificate-manager.php | 906 +++++++++++ .../class-certificate-security.php | 322 ++++ .../class-certificate-settings.php | 200 +++ .../class-certificate-template.php | 437 +++++ .../class-certificate-url-handler.php | 213 +++ includes/certificates/test-rewrite-rules.php | 71 + .../class-communication-installer.php | 383 +++++ .../class-communication-logger.php | 467 ++++++ .../class-communication-schedule-manager.php | 603 +++++++ .../class-communication-scheduler.php | 596 +++++++ .../class-communication-templates.php | 518 ++++++ .../class-communication-trigger-engine.php | 519 ++++++ .../community/class-email-attendees-data.php | 470 ++++++ includes/community/class-email-debug.php | 305 ++++ includes/community/class-event-handler.php | 62 + .../community/class-event-summary-data.php | 408 +++++ includes/community/class-login-handler.php | 265 +++ .../community/class-order-summary-data.php | 343 ++++ .../class-hvac-contact-submissions-table.php | 299 ++++ .../class-hvac-contact-form-handler.php | 603 +++++++ .../class-hvac-find-trainer-page.php | 570 +++++++ .../class-hvac-mapgeo-integration.php | 1419 +++++++++++++++++ .../class-hvac-trainer-directory-query.php | 732 +++++++++ .../class-google-sheets-admin.php | 540 +++++++ .../class-google-sheets-auth.php | 435 +++++ .../class-google-sheets-folder-manager.php | 390 +++++ .../class-google-sheets-manager.php | 660 ++++++++ .../google-sheets-config-template.php | 47 + .../google-sheets/google-sheets-config.php | 46 + includes/helpers/attendee-profile-link.php | 150 ++ includes/zoho/README.md | 134 ++ includes/zoho/STAGING-MODE.md | 95 ++ includes/zoho/TESTING.md | 97 ++ includes/zoho/auth-server.php | 57 + includes/zoho/check-permissions.php | 144 ++ includes/zoho/class-zoho-admin.php | 211 +++ includes/zoho/class-zoho-crm-auth.php | 427 +++++ includes/zoho/class-zoho-sync.php | 428 +++++ includes/zoho/diagnostics.php | 224 +++ includes/zoho/setup-helper.php | 155 ++ includes/zoho/test-integration.php | 217 +++ includes/zoho/zoho-config-template.php | 33 + includes/zoho/zoho-config.php | 229 +++ .../attendee/template-attendee-profile.php | 149 ++ templates/certificates/certificate-fix.php | 135 ++ .../certificate-reports-content.php | 230 +++ .../generate-certificates-content.php | 166 ++ .../template-certificate-reports-fixed.php | 427 +++++ .../template-certificate-reports-simple.php | 268 ++++ .../template-certificate-reports.php | 407 +++++ .../template-certificate-reports.php.backup | 328 ++++ .../template-certificate-reports.php.bak | 346 ++++ .../template-generate-certificates-fixed.php | 461 ++++++ .../template-generate-certificates.php | 534 +++++++ .../template-generate-certificates.php.backup | 541 +++++++ .../template-communication-schedules.php | 832 ++++++++++ .../template-communication-templates.php | 673 ++++++++ .../communication/template-manager-widget.php | 180 +++ templates/community/login-form.php | 104 ++ .../content/trainer-account-disabled.html | 65 + .../content/trainer-account-pending.html | 59 + .../template-email-attendees.php | 385 +++++ .../event-summary/template-event-summary.php | 489 ++++++ templates/status/trainer-account-disabled.php | 36 + templates/status/trainer-account-pending.php | 36 + templates/template-parts/trainer-header.php | 32 + 73 files changed, 25913 insertions(+) create mode 100644 includes/admin/class-admin-dashboard.php create mode 100644 includes/admin/init_hooks_replacement.txt create mode 100644 includes/certificates/class-certificate-ajax-handler.php create mode 100644 includes/certificates/class-certificate-fix.php create mode 100644 includes/certificates/class-certificate-generator.php create mode 100644 includes/certificates/class-certificate-installer.php create mode 100644 includes/certificates/class-certificate-manager-broken.php create mode 100644 includes/certificates/class-certificate-manager.php create mode 100644 includes/certificates/class-certificate-security.php create mode 100644 includes/certificates/class-certificate-settings.php create mode 100644 includes/certificates/class-certificate-template.php create mode 100644 includes/certificates/class-certificate-url-handler.php create mode 100644 includes/certificates/test-rewrite-rules.php create mode 100644 includes/communication/class-communication-installer.php create mode 100644 includes/communication/class-communication-logger.php create mode 100644 includes/communication/class-communication-schedule-manager.php create mode 100644 includes/communication/class-communication-scheduler.php create mode 100644 includes/communication/class-communication-templates.php create mode 100644 includes/communication/class-communication-trigger-engine.php create mode 100644 includes/community/class-email-attendees-data.php create mode 100644 includes/community/class-email-debug.php create mode 100644 includes/community/class-event-handler.php create mode 100644 includes/community/class-event-summary-data.php create mode 100644 includes/community/class-login-handler.php create mode 100644 includes/community/class-order-summary-data.php create mode 100644 includes/database/class-hvac-contact-submissions-table.php create mode 100644 includes/find-trainer/class-hvac-contact-form-handler.php create mode 100644 includes/find-trainer/class-hvac-find-trainer-page.php create mode 100644 includes/find-trainer/class-hvac-mapgeo-integration.php create mode 100644 includes/find-trainer/class-hvac-trainer-directory-query.php create mode 100644 includes/google-sheets/class-google-sheets-admin.php create mode 100644 includes/google-sheets/class-google-sheets-auth.php create mode 100644 includes/google-sheets/class-google-sheets-folder-manager.php create mode 100644 includes/google-sheets/class-google-sheets-manager.php create mode 100644 includes/google-sheets/google-sheets-config-template.php create mode 100644 includes/google-sheets/google-sheets-config.php create mode 100644 includes/helpers/attendee-profile-link.php create mode 100644 includes/zoho/README.md create mode 100644 includes/zoho/STAGING-MODE.md create mode 100644 includes/zoho/TESTING.md create mode 100644 includes/zoho/auth-server.php create mode 100644 includes/zoho/check-permissions.php create mode 100644 includes/zoho/class-zoho-admin.php create mode 100644 includes/zoho/class-zoho-crm-auth.php create mode 100644 includes/zoho/class-zoho-sync.php create mode 100644 includes/zoho/diagnostics.php create mode 100644 includes/zoho/setup-helper.php create mode 100644 includes/zoho/test-integration.php create mode 100644 includes/zoho/zoho-config-template.php create mode 100644 includes/zoho/zoho-config.php create mode 100644 templates/attendee/template-attendee-profile.php create mode 100644 templates/certificates/certificate-fix.php create mode 100644 templates/certificates/certificate-reports-content.php create mode 100644 templates/certificates/generate-certificates-content.php create mode 100644 templates/certificates/template-certificate-reports-fixed.php create mode 100644 templates/certificates/template-certificate-reports-simple.php create mode 100644 templates/certificates/template-certificate-reports.php create mode 100644 templates/certificates/template-certificate-reports.php.backup create mode 100644 templates/certificates/template-certificate-reports.php.bak create mode 100644 templates/certificates/template-generate-certificates-fixed.php create mode 100644 templates/certificates/template-generate-certificates.php create mode 100644 templates/certificates/template-generate-certificates.php.backup create mode 100644 templates/communication/template-communication-schedules.php create mode 100644 templates/communication/template-communication-templates.php create mode 100644 templates/communication/template-manager-widget.php create mode 100644 templates/community/login-form.php create mode 100644 templates/content/trainer-account-disabled.html create mode 100644 templates/content/trainer-account-pending.html create mode 100644 templates/email-attendees/template-email-attendees.php create mode 100644 templates/event-summary/template-event-summary.php create mode 100644 templates/status/trainer-account-disabled.php create mode 100644 templates/status/trainer-account-pending.php create mode 100644 templates/template-parts/trainer-header.php diff --git a/includes/admin/class-admin-dashboard.php b/includes/admin/class-admin-dashboard.php new file mode 100644 index 00000000..1dc57e30 --- /dev/null +++ b/includes/admin/class-admin-dashboard.php @@ -0,0 +1,858 @@ + +
+

+ + render_health_check(); ?> + +
+ render_trainer_metrics(); ?> + render_event_statistics(); ?> + render_revenue_statistics(); ?> + render_maintenance_controls(); ?> +
+ +
+ + +
+
+ get_health_status(); + ?> +
+

+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + + +
+
+ get_trainer_metrics(); + ?> +
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ get_event_statistics(); + ?> +
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ get_revenue_statistics(); + ?> +
+

+
+
+
$
+
+
+
+
$
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+

+
+ + + + +
+ +
+ 'The Events Calendar', + 'status' => $tec_active ? 'ok' : 'error', + 'status_text' => $tec_active ? __('Active', 'hvac-ce') : __('Inactive', 'hvac-ce'), + 'details' => $tec_active ? __('Plugin is active and functioning', 'hvac-ce') : __('Required plugin is not active', 'hvac-ce') + ); + + if (!$tec_active) { + $overall_status = 'critical'; + } + + // Check Community Events + $ce_active = class_exists('Tribe__Events__Community__Main'); + $checks[] = array( + 'component' => 'Community Events', + 'status' => $ce_active ? 'ok' : 'warning', + 'status_text' => $ce_active ? __('Active', 'hvac-ce') : __('Inactive', 'hvac-ce'), + 'details' => $ce_active ? __('Plugin is active', 'hvac-ce') : __('Recommended plugin is not active', 'hvac-ce') + ); + + if (!$ce_active && $overall_status !== 'critical') { + $overall_status = 'warning'; + } + + // Check database tables + global $wpdb; + $tables_exist = $wpdb->get_var("SHOW TABLES LIKE '{$wpdb->prefix}posts'") === "{$wpdb->prefix}posts"; + $checks[] = array( + 'component' => 'Database Tables', + 'status' => $tables_exist ? 'ok' : 'error', + 'status_text' => $tables_exist ? __('OK', 'hvac-ce') : __('Error', 'hvac-ce'), + 'details' => $tables_exist ? __('All required tables exist', 'hvac-ce') : __('Missing required database tables', 'hvac-ce') + ); + + // Check file permissions + $upload_dir = wp_upload_dir(); + $uploads_writable = wp_is_writable($upload_dir['basedir']); + $checks[] = array( + 'component' => 'File Permissions', + 'status' => $uploads_writable ? 'ok' : 'warning', + 'status_text' => $uploads_writable ? __('OK', 'hvac-ce') : __('Warning', 'hvac-ce'), + 'details' => $uploads_writable ? __('Upload directory is writable', 'hvac-ce') : __('Upload directory is not writable', 'hvac-ce') + ); + + // Memory limit check + $memory_limit = wp_convert_hr_to_bytes(ini_get('memory_limit')); + $recommended_limit = 256 * MB_IN_BYTES; + $checks[] = array( + 'component' => 'Memory Limit', + 'status' => $memory_limit >= $recommended_limit ? 'ok' : 'warning', + 'status_text' => size_format($memory_limit), + 'details' => $memory_limit >= $recommended_limit + ? __('Memory limit is sufficient', 'hvac-ce') + : sprintf(__('Recommended: %s or higher', 'hvac-ce'), size_format($recommended_limit)) + ); + + return array( + 'overall_status' => $overall_status, + 'status_text' => $this->get_status_text($overall_status), + 'checks' => $checks + ); + } + + /** + * Get status text + */ + private function get_status_text($status) { + switch ($status) { + case 'healthy': + return __('All Systems Operational', 'hvac-ce'); + case 'warning': + return __('Minor Issues Detected', 'hvac-ce'); + case 'critical': + return __('Critical Issues Found', 'hvac-ce'); + default: + return __('Unknown Status', 'hvac-ce'); + } + } + + /** + * Get trainer metrics + */ + private function get_trainer_metrics() { + global $wpdb; + + // Total trainers + $total_trainers = count(get_users(array( + 'role' => 'trainer', + 'fields' => 'ID' + ))); + + // New trainers this week + $week_ago = date('Y-m-d H:i:s', strtotime('-1 week')); + $new_trainers_week = count(get_users(array( + 'role' => 'trainer', + 'date_query' => array( + array( + 'after' => $week_ago, + 'inclusive' => true + ) + ), + 'fields' => 'ID' + ))); + + // Login statistics (would require custom tracking) + $total_logins = get_option('hvac_total_logins', 0); + $logins_week = get_option('hvac_logins_week', 0); + + return array( + 'total_trainers' => $total_trainers, + 'new_trainers_week' => $new_trainers_week, + 'total_logins' => $total_logins, + 'logins_week' => $logins_week + ); + } + + /** + * Get event statistics + */ + private function get_event_statistics() { + global $wpdb; + + $now = current_time('mysql'); + + // Total events + $total_events = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(*) FROM {$wpdb->posts} p + WHERE p.post_type = %s + AND p.post_status IN ('publish', 'draft', 'private') + ", 'tribe_events')); + + // Past events + $past_events = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p + JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm.meta_key = '_EventEndDate' + AND pm.meta_value < %s + ", 'tribe_events', $now)); + + // Future events + $future_events = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p + JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + WHERE p.post_type = %s + AND p.post_status = 'publish' + AND pm.meta_key = '_EventStartDate' + AND pm.meta_value > %s + ", 'tribe_events', $now)); + + // Draft events + $draft_events = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(*) FROM {$wpdb->posts} + WHERE post_type = %s + AND post_status = 'draft' + ", 'tribe_events')); + + // Cancelled events (assuming there's a meta field for cancelled status) + $cancelled_events = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(DISTINCT p.ID) FROM {$wpdb->posts} p + JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + WHERE p.post_type = %s + AND pm.meta_key = '_event_cancelled' + AND pm.meta_value = '1' + ", 'tribe_events')); + + // Total attendees using Event Tickets data + $total_attendees = 0; + + // Check if Event Tickets is active + if (class_exists('Tribe__Tickets__Main')) { + // Get all attendee post types from Event Tickets + $attendee_types = [ + 'tribe_rsvp_attendees', // RSVP attendees + 'tribe_tpp_attendees', // PayPal attendees + 'tec_tc_attendee' // Tickets Commerce attendees + ]; + + // Preparing for the SQL query + $types_placeholder = implode(', ', array_fill(0, count($attendee_types), '%s')); + $query_args = $attendee_types; + + // Add status condition - Public order statuses + // (based on Tribe__Tickets__Attendee_Repository class) + $public_order_statuses = [ + 'yes', // RSVP + 'completed', // PayPal Legacy + 'wc-completed', // WooCommerce + 'publish', // Easy Digital Downloads, Legacy + 'complete', // Easy Digital Downloads + ]; + + // Count attendees with proper status + foreach ($attendee_types as $post_type) { + $count = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(*) FROM {$wpdb->posts} + WHERE post_type = %s + AND post_status = 'publish' + ", $post_type)); + + $total_attendees += (int)$count; + } + + // If WooCommerce Tickets is active, count WooCommerce ticket attendees too + if (class_exists('Tribe__Tickets_Plus__Commerce__WooCommerce__Main')) { + $wc_attendees = $wpdb->get_var(" + SELECT COUNT(*) FROM {$wpdb->postmeta} + WHERE meta_key = '_tribe_wooticket_attendance' + "); + $total_attendees += (int)$wc_attendees; + } + } + + return array( + 'total_events' => $total_events, + 'past_events' => $past_events, + 'future_events' => $future_events, + 'draft_events' => $draft_events, + 'cancelled_events' => $cancelled_events, + 'total_attendees' => $total_attendees + ); + } + + /** + * Get revenue statistics + */ + private function get_revenue_statistics() { + global $wpdb; + + $week_ago = date('Y-m-d H:i:s', strtotime('-1 week')); + + $total_revenue = 0; + $revenue_week = 0; + $total_purchases = 0; + $purchases_week = 0; + + // If using Event Tickets Plus with WooCommerce + if (class_exists('Tribe__Tickets_Plus__Commerce__WooCommerce__Main')) { + // Gather data from WooCommerce orders that contain tickets + // First, find all orders with ticket items + $ticket_product_ids = $wpdb->get_col(" + SELECT ID FROM {$wpdb->posts} + WHERE post_type = 'tribe_wooticket' + "); + + if (!empty($ticket_product_ids)) { + $ticket_product_ids_str = implode(',', array_map('intval', $ticket_product_ids)); + + // Find orders that contain ticket products + $ticket_order_ids = $wpdb->get_col(" + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items oi + JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim ON oi.order_item_id = oim.order_item_id + WHERE oim.meta_key = '_product_id' + AND oim.meta_value IN ({$ticket_product_ids_str}) + "); + + if (!empty($ticket_order_ids)) { + $ticket_order_ids_str = implode(',', array_map('intval', $ticket_order_ids)); + + // Total revenue from these orders + $total_revenue = $wpdb->get_var(" + SELECT SUM(meta.meta_value) + FROM {$wpdb->postmeta} meta + JOIN {$wpdb->posts} posts ON meta.post_id = posts.ID + WHERE meta.meta_key = '_order_total' + AND posts.ID IN ({$ticket_order_ids_str}) + AND posts.post_status IN ('wc-completed', 'wc-processing') + "); + + // Revenue this week + $revenue_week = $wpdb->get_var($wpdb->prepare(" + SELECT SUM(meta.meta_value) + FROM {$wpdb->postmeta} meta + JOIN {$wpdb->posts} posts ON meta.post_id = posts.ID + WHERE meta.meta_key = '_order_total' + AND posts.ID IN ({$ticket_order_ids_str}) + AND posts.post_status IN ('wc-completed', 'wc-processing') + AND posts.post_date >= %s + ", $week_ago)); + + // Total purchases (count of orders) + $total_purchases = count($ticket_order_ids); + + // Purchases this week + $purchases_week = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(*) + FROM {$wpdb->posts} + WHERE ID IN ({$ticket_order_ids_str}) + AND post_status IN ('wc-completed', 'wc-processing') + AND post_date >= %s + ", $week_ago)); + } + } + } + + // Check for Tickets Commerce data (modern Event Tickets) + if (class_exists('TEC\\Tickets\\Commerce\\Order')) { + // Get orders through Tickets Commerce + $tc_order_post_type = \TEC\Tickets\Commerce\Order::POSTTYPE; + + $tc_completed_statuses = [ + 'completed', 'pfc-completed', 'tpay-completed', 'paid' + ]; + + $placeholders = implode(', ', array_fill(0, count($tc_completed_statuses), '%s')); + $query_args = $tc_completed_statuses; + + // Calculate total revenue + $tc_total_revenue = $wpdb->get_var($wpdb->prepare(" + SELECT SUM(meta.meta_value) + FROM {$wpdb->postmeta} meta + JOIN {$wpdb->posts} posts ON meta.post_id = posts.ID + WHERE meta.meta_key = '_tec_tc_order_total' + AND posts.post_type = %s + AND posts.post_status IN ($placeholders) + ", array_merge([$tc_order_post_type], $query_args))); + + // Calculate this week's revenue + $query_args[] = $week_ago; + $tc_revenue_week = $wpdb->get_var($wpdb->prepare(" + SELECT SUM(meta.meta_value) + FROM {$wpdb->postmeta} meta + JOIN {$wpdb->posts} posts ON meta.post_id = posts.ID + WHERE meta.meta_key = '_tec_tc_order_total' + AND posts.post_type = %s + AND posts.post_status IN ($placeholders) + AND posts.post_date >= %s + ", array_merge([$tc_order_post_type], $query_args))); + + // Count total purchases + $tc_total_purchases = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(*) + FROM {$wpdb->posts} + WHERE post_type = %s + AND post_status IN ($placeholders) + ", array_merge([$tc_order_post_type], $tc_completed_statuses))); + + // Count purchases this week + $tc_purchases_week = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(*) + FROM {$wpdb->posts} + WHERE post_type = %s + AND post_status IN ($placeholders) + AND post_date >= %s + ", array_merge([$tc_order_post_type], $tc_completed_statuses, [$week_ago]))); + + // Add TC values to totals + $total_revenue += (float)$tc_total_revenue; + $revenue_week += (float)$tc_revenue_week; + $total_purchases += (int)$tc_total_purchases; + $purchases_week += (int)$tc_purchases_week; + } + + // Tribe Commerce PayPal (legacy from Event Tickets) + if (class_exists('Tribe__Tickets__Commerce__PayPal__Main')) { + // PayPal orders are stored as posts with meta data + $pp_completed_status = 'completed'; + + // Calculate total revenue + $pp_total_revenue = $wpdb->get_var($wpdb->prepare(" + SELECT SUM(meta.meta_value) + FROM {$wpdb->postmeta} meta + JOIN {$wpdb->postmeta} status ON status.post_id = meta.post_id + WHERE meta.meta_key = '_tribe_tpp_gross' + AND status.meta_key = '_tribe_tpp_status' + AND status.meta_value = %s + ", $pp_completed_status)); + + // Calculate this week's revenue + $pp_revenue_week = $wpdb->get_var($wpdb->prepare(" + SELECT SUM(meta.meta_value) + FROM {$wpdb->postmeta} meta + JOIN {$wpdb->postmeta} status ON status.post_id = meta.post_id + JOIN {$wpdb->posts} posts ON meta.post_id = posts.ID + WHERE meta.meta_key = '_tribe_tpp_gross' + AND status.meta_key = '_tribe_tpp_status' + AND status.meta_value = %s + AND posts.post_date >= %s + ", $pp_completed_status, $week_ago)); + + // Count total PayPal orders + $pp_total_purchases = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(DISTINCT post_id) + FROM {$wpdb->postmeta} + WHERE meta_key = '_tribe_tpp_status' + AND meta_value = %s + ", $pp_completed_status)); + + // Count purchases this week + $pp_purchases_week = $wpdb->get_var($wpdb->prepare(" + SELECT COUNT(DISTINCT meta.post_id) + FROM {$wpdb->postmeta} meta + JOIN {$wpdb->posts} posts ON meta.post_id = posts.ID + WHERE meta.meta_key = '_tribe_tpp_status' + AND meta.meta_value = %s + AND posts.post_date >= %s + ", $pp_completed_status, $week_ago)); + + // Add PayPal values to totals + $total_revenue += (float)$pp_total_revenue; + $revenue_week += (float)$pp_revenue_week; + $total_purchases += (int)$pp_total_purchases; + $purchases_week += (int)$pp_purchases_week; + } + + return array( + 'total_revenue' => $total_revenue ?: 0, + 'revenue_week' => $revenue_week ?: 0, + 'total_purchases' => $total_purchases ?: 0, + 'purchases_week' => $purchases_week ?: 0 + ); + } + + /** + * AJAX handler for refreshing metrics + */ + public function ajax_refresh_metrics() { + check_ajax_referer('hvac_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_die('Unauthorized'); + } + + $metrics = array( + 'trainer' => $this->get_trainer_metrics(), + 'events' => $this->get_event_statistics(), + 'revenue' => $this->get_revenue_statistics() + ); + + wp_send_json_success($metrics); + } + + /** + * AJAX handler for exporting metrics + */ + public function ajax_export_metrics() { + check_ajax_referer('hvac_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_die('Unauthorized'); + } + + $metrics = array( + 'trainer' => $this->get_trainer_metrics(), + 'events' => $this->get_event_statistics(), + 'revenue' => $this->get_revenue_statistics() + ); + + // Generate CSV + $csv_data = array(); + + // Headers + $csv_data[] = array('Metric Category', 'Metric', 'Value'); + + // Trainer metrics + foreach ($metrics['trainer'] as $key => $value) { + $csv_data[] = array('Trainer Metrics', $this->humanize_key($key), $value); + } + + // Event statistics + foreach ($metrics['events'] as $key => $value) { + $csv_data[] = array('Event Statistics', $this->humanize_key($key), $value); + } + + // Revenue statistics + foreach ($metrics['revenue'] as $key => $value) { + if (strpos($key, 'revenue') !== false) { + $value = '$' . number_format($value, 2); + } + $csv_data[] = array('Revenue Statistics', $this->humanize_key($key), $value); + } + + // Add timestamp + $csv_data[] = array('Export Date', date('Y-m-d H:i:s'), ''); + + wp_send_json_success(array( + 'csv' => $csv_data, + 'filename' => 'hvac-metrics-' . date('Y-m-d') . '.csv' + )); + } + + /** + * AJAX handler for maintenance actions + */ + public function ajax_run_maintenance() { + check_ajax_referer('hvac_admin_nonce', 'nonce'); + + if (!current_user_can('manage_options')) { + wp_die('Unauthorized'); + } + + $action = sanitize_text_field($_POST['action_type']); + $result = array(); + + switch ($action) { + case 'clear_transients': + $result = $this->clear_transients(); + break; + + case 'optimize_tables': + $result = $this->optimize_tables(); + break; + + case 'regenerate_roles': + $result = $this->regenerate_roles(); + break; + + case 'sync_event_meta': + $result = $this->sync_event_metadata(); + break; + + default: + $result = array( + 'success' => false, + 'message' => __('Invalid maintenance action', 'hvac-ce') + ); + } + + if ($result['success']) { + wp_send_json_success($result); + } else { + wp_send_json_error($result); + } + } + + /** + * Clear transients + */ + private function clear_transients() { + global $wpdb; + + // Clear all transients + $query = "DELETE FROM {$wpdb->options} + WHERE option_name LIKE '_transient_%' + OR option_name LIKE '_site_transient_%'"; + + $deleted = $wpdb->query($query); + + // Clear object cache + wp_cache_flush(); + + return array( + 'success' => true, + 'message' => sprintf(__('Cleared %d transients and flushed object cache', 'hvac-ce'), $deleted) + ); + } + + /** + * Optimize database tables + */ + private function optimize_tables() { + global $wpdb; + + $tables = array( + $wpdb->posts, + $wpdb->postmeta, + $wpdb->users, + $wpdb->usermeta, + $wpdb->options + ); + + $optimized = 0; + foreach ($tables as $table) { + if ($wpdb->query("OPTIMIZE TABLE $table")) { + $optimized++; + } + } + + return array( + 'success' => true, + 'message' => sprintf(__('Optimized %d database tables', 'hvac-ce'), $optimized) + ); + } + + /** + * Regenerate user roles + */ + private function regenerate_roles() { + // Re-add custom roles + $role_manager = new HVAC_Role_Manager(); + $role_manager->add_roles(); + + return array( + 'success' => true, + 'message' => __('User roles regenerated successfully', 'hvac-ce') + ); + } + + /** + * Sync event metadata + */ + private function sync_event_metadata() { + global $wpdb; + + // Example: Ensure all events have required metadata + $events = get_posts(array( + 'post_type' => 'tribe_events', + 'posts_per_page' => -1, + 'post_status' => array('publish', 'draft', 'private') + )); + + $synced = 0; + foreach ($events as $event) { + // Check for required meta fields + if (!get_post_meta($event->ID, '_EventStartDate', true)) { + // Set default start date if missing + update_post_meta($event->ID, '_EventStartDate', current_time('mysql')); + $synced++; + } + } + + return array( + 'success' => true, + 'message' => sprintf(__('Synced metadata for %d events', 'hvac-ce'), $synced) + ); + } + + /** + * Humanize key for display + */ + private function humanize_key($key) { + $key = str_replace('_', ' ', $key); + return ucwords($key); + } +} \ No newline at end of file diff --git a/includes/admin/init_hooks_replacement.txt b/includes/admin/init_hooks_replacement.txt new file mode 100644 index 00000000..ee3e7b99 --- /dev/null +++ b/includes/admin/init_hooks_replacement.txt @@ -0,0 +1,15 @@ + /** + * Initialize hooks + */ + private function init_hooks() { + // Register activation/deactivation hooks + // Note: These hooks are typically registered outside the class instance context + // register_activation_hook(__FILE__, array($this, 'activate')); // This won't work correctly here + // register_deactivation_hook(__FILE__, array($this, 'deactivate')); // This won't work correctly here + + // Initialize other hooks + add_action('init', array($this, 'init')); + + // Template loading for custom pages + add_filter('template_include', array($this, 'load_custom_templates')); + } // End init_hooks \ No newline at end of file diff --git a/includes/certificates/class-certificate-ajax-handler.php b/includes/certificates/class-certificate-ajax-handler.php new file mode 100644 index 00000000..39ac7e78 --- /dev/null +++ b/includes/certificates/class-certificate-ajax-handler.php @@ -0,0 +1,541 @@ +certificate_manager = HVAC_Certificate_Manager::instance(); + $this->certificate_security = HVAC_Certificate_Security::instance(); + + // Initialize hooks + $this->init_hooks(); + } + + /** + * Initialize hooks. + */ + protected function init_hooks() { + // Register AJAX handlers + add_action('wp_ajax_hvac_get_certificate_url', array($this, 'get_certificate_url')); + add_action('wp_ajax_hvac_email_certificate', array($this, 'email_certificate')); + add_action('wp_ajax_hvac_revoke_certificate', array($this, 'revoke_certificate')); + add_action('wp_ajax_hvac_generate_certificates', array($this, 'generate_certificates')); + add_action('wp_ajax_hvac_get_event_attendees', array($this, 'get_event_attendees')); + + // Enqueue scripts + add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); + } + + /** + * Enqueue scripts and localize data. + */ + public function enqueue_scripts() { + // Only load on certificate pages + if (is_page('certificate-reports') || is_page('generate-certificates')) { + // Enqueue UX enhancements first + wp_enqueue_style( + 'hvac-ux-enhancements-css', + HVAC_PLUGIN_URL . 'assets/css/hvac-ux-enhancements.css', + array(), + HVAC_PLUGIN_VERSION + ); + + wp_enqueue_script( + 'hvac-ux-enhancements-js', + HVAC_PLUGIN_URL . 'assets/js/hvac-ux-enhancements.js', + array('jquery'), + HVAC_PLUGIN_VERSION, + true + ); + + // Enqueue certificate actions JS + wp_enqueue_script( + 'hvac-certificate-actions-js', + HVAC_PLUGIN_URL . 'assets/js/hvac-certificate-actions.js', + array('jquery', 'hvac-ux-enhancements-js'), + HVAC_PLUGIN_VERSION, + true + ); + + // Localize script with AJAX data + wp_localize_script('hvac-certificate-actions-js', 'hvacCertificateData', array( + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'viewNonce' => wp_create_nonce('hvac_view_certificate'), + 'emailNonce' => wp_create_nonce('hvac_email_certificate'), + 'revokeNonce' => wp_create_nonce('hvac_revoke_certificate'), + 'generateNonce' => wp_create_nonce('hvac_generate_certificates') + )); + } + } + + /** + * AJAX handler for getting a certificate download URL. + */ + public function get_certificate_url() { + // Verify nonce + if ( + (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_view_certificate')) && + (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_certificate_actions')) + ) { + wp_send_json_error(array('message' => 'Security check failed')); + } + + // Get certificate by different methods + $certificate = null; + + // Method 1: Direct certificate ID + if (isset($_POST['certificate_id']) && absint($_POST['certificate_id'])) { + $certificate_id = absint($_POST['certificate_id']); + $certificate = $this->certificate_manager->get_certificate($certificate_id); + } + // Method 2: Event ID and Attendee ID + elseif (isset($_POST['event_id']) && isset($_POST['attendee_id'])) { + $event_id = absint($_POST['event_id']); + $attendee_id = absint($_POST['attendee_id']); + $certificate = $this->certificate_manager->get_certificate_by_attendee($event_id, $attendee_id); + } else { + wp_send_json_error(array('message' => 'Missing certificate information')); + } + + // Check if certificate exists + if (!$certificate) { + wp_send_json_error(array('message' => 'Certificate not found')); + } + + // Shorthand for certificate ID + $certificate_id = $certificate->certificate_id; + + // Check user permissions (must be the event author or admin) + $event = get_post($certificate->event_id); + + if (!$event || !current_user_can('edit_post', $event->ID)) { + wp_send_json_error(array('message' => 'You do not have permission to view this certificate')); + } + + // Get attendee name + $attendee_name = get_post_meta($certificate->attendee_id, '_tribe_tickets_full_name', true); + if (empty($attendee_name)) { + $attendee_name = 'Attendee #' . $certificate->attendee_id; + } + + // Generate secure download URL + $certificate_data = array( + 'file_path' => $certificate->file_path, + 'event_name' => $event->post_title, + 'attendee_name' => $attendee_name + ); + + $download_url = $this->certificate_security->generate_download_token($certificate_id, $certificate_data); + + if (!$download_url) { + wp_send_json_error(array('message' => 'Failed to generate download URL')); + } + + wp_send_json_success(array('url' => $download_url)); + } + + /** + * AJAX handler for emailing a certificate. + */ + public function email_certificate() { + // Verify nonce + if ( + (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_email_certificate')) && + (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_certificate_actions')) + ) { + wp_send_json_error(array('message' => 'Security check failed')); + } + + // Get certificate by different methods + $certificate = null; + + // Method 1: Direct certificate ID + if (isset($_POST['certificate_id']) && absint($_POST['certificate_id'])) { + $certificate_id = absint($_POST['certificate_id']); + $certificate = $this->certificate_manager->get_certificate($certificate_id); + } + // Method 2: Event ID and Attendee ID + elseif (isset($_POST['event_id']) && isset($_POST['attendee_id'])) { + $event_id = absint($_POST['event_id']); + $attendee_id = absint($_POST['attendee_id']); + $certificate = $this->certificate_manager->get_certificate_by_attendee($event_id, $attendee_id); + } else { + wp_send_json_error(array('message' => 'Missing certificate information')); + } + + // Check if certificate exists + if (!$certificate) { + wp_send_json_error(array('message' => 'Certificate not found')); + } + + // Shorthand for certificate ID + $certificate_id = $certificate->certificate_id; + + // Check if certificate is revoked + if ($certificate->revoked) { + wp_send_json_error(array('message' => 'Cannot email a revoked certificate')); + } + + // Check user permissions (must be the event author or admin) + $event = get_post($certificate->event_id); + + if (!$event || !current_user_can('edit_post', $event->ID)) { + wp_send_json_error(array('message' => 'You do not have permission to email this certificate')); + } + + // Get attendee email + $attendee_email = get_post_meta($certificate->attendee_id, '_tribe_tickets_email', true); + + if (empty($attendee_email)) { + wp_send_json_error(array('message' => 'Attendee email not found')); + } + + // Get attendee name + $attendee_name = get_post_meta($certificate->attendee_id, '_tribe_tickets_full_name', true); + if (empty($attendee_name)) { + $attendee_name = 'Attendee'; + } + + // Generate secure download URL (expires in 7 days) + $certificate_data = array( + 'file_path' => $certificate->file_path, + 'event_name' => $event->post_title, + 'attendee_name' => $attendee_name + ); + + $download_url = $this->certificate_security->generate_download_token($certificate_id, $certificate_data, 7 * DAY_IN_SECONDS); + + if (!$download_url) { + wp_send_json_error(array('message' => 'Failed to generate download URL')); + } + + // Get current user (sender) info + $sender_name = wp_get_current_user()->display_name; + + // Email subject + $subject = sprintf( + __('Your Certificate for %s', 'hvac-community-events'), + $event->post_title + ); + + // Email body + $message = sprintf( + __("Hello %s,\n\nThank you for attending %s.\n\nYour certificate of completion is now available. Please click the link below to download your certificate:\n\n%s\n\nThis link will expire in 7 days.\n\nRegards,\n%s", 'hvac-community-events'), + $attendee_name, + $event->post_title, + $download_url, + $sender_name + ); + + // Send email + $headers = array('Content-Type: text/plain; charset=UTF-8'); + $sent = wp_mail($attendee_email, $subject, $message, $headers); + + if ($sent) { + // Record email sent + $this->certificate_manager->mark_certificate_emailed($certificate_id); + + wp_send_json_success(array('message' => 'Certificate sent successfully')); + } else { + wp_send_json_error(array('message' => 'Failed to send email')); + } + } + + /** + * AJAX handler for revoking a certificate. + */ + public function revoke_certificate() { + // Verify nonce + if ( + (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_revoke_certificate')) && + (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_certificate_actions')) + ) { + wp_send_json_error(array('message' => 'Security check failed')); + } + + // Get reason for revocation + $reason = isset($_POST['reason']) ? sanitize_text_field($_POST['reason']) : ''; + + // Get certificate by different methods + $certificate = null; + + // Method 1: Direct certificate ID + if (isset($_POST['certificate_id']) && absint($_POST['certificate_id'])) { + $certificate_id = absint($_POST['certificate_id']); + $certificate = $this->certificate_manager->get_certificate($certificate_id); + } + // Method 2: Event ID and Attendee ID + elseif (isset($_POST['event_id']) && isset($_POST['attendee_id'])) { + $event_id = absint($_POST['event_id']); + $attendee_id = absint($_POST['attendee_id']); + $certificate = $this->certificate_manager->get_certificate_by_attendee($event_id, $attendee_id); + } else { + wp_send_json_error(array('message' => 'Missing certificate information')); + } + + // Check if certificate exists + if (!$certificate) { + wp_send_json_error(array('message' => 'Certificate not found')); + } + + // Shorthand for certificate ID + $certificate_id = $certificate->certificate_id; + + // Check if certificate is already revoked + if ($certificate->revoked) { + wp_send_json_error(array('message' => 'Certificate is already revoked')); + } + + // Check user permissions (must be the event author or admin) + $event = get_post($certificate->event_id); + + if (!$event || !current_user_can('edit_post', $event->ID)) { + wp_send_json_error(array('message' => 'You do not have permission to revoke this certificate')); + } + + // Revoke the certificate + $revoked = $this->certificate_manager->revoke_certificate( + $certificate_id, + get_current_user_id(), + $reason + ); + + if ($revoked) { + // Get updated certificate for revocation date + $updated_certificate = $this->certificate_manager->get_certificate($certificate_id); + $revoked_date = date_i18n(get_option('date_format'), strtotime($updated_certificate->revoked_date)); + + wp_send_json_success(array( + 'message' => 'Certificate revoked successfully', + 'revoked_date' => $revoked_date + )); + } else { + wp_send_json_error(array('message' => 'Failed to revoke certificate')); + } + } + + /** + * AJAX handler for getting event attendees. + */ + public function get_event_attendees() { + // Verify nonce + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_generate_certificates')) { + wp_send_json_error(array('message' => 'Security check failed')); + } + + $event_id = isset($_POST['event_id']) ? absint($_POST['event_id']) : 0; + + if (!$event_id) { + wp_send_json_error(array('message' => 'Event ID is required')); + } + + // Check user permissions + $event = get_post($event_id); + if (!$event || !current_user_can('edit_post', $event->ID)) { + wp_send_json_error(array('message' => 'You do not have permission to view this event')); + } + + // Get attendees using direct database query (same as Generate Certificates template) + global $wpdb; + $tec_attendees = $wpdb->get_results($wpdb->prepare( + "SELECT + p.ID as attendee_id, + p.post_parent as event_id, + COALESCE(tec_full_name.meta_value, tpp_full_name.meta_value, tickets_full_name.meta_value, 'Unknown Attendee') as holder_name, + COALESCE(tec_email.meta_value, tpp_email.meta_value, tickets_email.meta_value, tpp_attendee_email.meta_value, 'no-email@example.com') as holder_email, + COALESCE(checked_in.meta_value, '0') as check_in + FROM {$wpdb->posts} p + LEFT JOIN {$wpdb->postmeta} tec_full_name ON p.ID = tec_full_name.post_id AND tec_full_name.meta_key = '_tec_tickets_commerce_full_name' + LEFT JOIN {$wpdb->postmeta} tpp_full_name ON p.ID = tpp_full_name.post_id AND tpp_full_name.meta_key = '_tribe_tpp_full_name' + LEFT JOIN {$wpdb->postmeta} tickets_full_name ON p.ID = tickets_full_name.post_id AND tickets_full_name.meta_key = '_tribe_tickets_full_name' + LEFT JOIN {$wpdb->postmeta} tec_email ON p.ID = tec_email.post_id AND tec_email.meta_key = '_tec_tickets_commerce_email' + LEFT JOIN {$wpdb->postmeta} tpp_email ON p.ID = tpp_email.post_id AND tpp_email.meta_key = '_tribe_tpp_email' + LEFT JOIN {$wpdb->postmeta} tickets_email ON p.ID = tickets_email.post_id AND tickets_email.meta_key = '_tribe_tickets_email' + LEFT JOIN {$wpdb->postmeta} tpp_attendee_email ON p.ID = tpp_attendee_email.post_id AND tpp_attendee_email.meta_key = '_tribe_tpp_attendee_email' + LEFT JOIN {$wpdb->postmeta} checked_in ON p.ID = checked_in.post_id AND checked_in.meta_key = '_tribe_tickets_attendee_checked_in' + WHERE p.post_type IN ('tec_tc_attendee', 'tribe_tpp_attendees') + AND p.post_parent = %d + ORDER BY p.ID ASC", + $event_id + )); + + $attendees = array(); + foreach ($tec_attendees as $attendee) { + // Check if certificate already exists + $has_certificate = $this->certificate_manager->certificate_exists($event_id, $attendee->attendee_id); + + $attendees[] = array( + 'attendee_id' => $attendee->attendee_id, + 'event_id' => $attendee->event_id, + 'holder_name' => $attendee->holder_name, + 'holder_email' => $attendee->holder_email, + 'check_in' => intval($attendee->check_in), + 'has_certificate' => $has_certificate + ); + } + + wp_send_json_success(array( + 'attendees' => $attendees, + 'event_title' => $event->post_title + )); + } + + /** + * AJAX handler for generating certificates. + */ + public function generate_certificates() { + // Verify nonce + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_generate_certificates')) { + wp_send_json_error(array('message' => 'Security check failed')); + } + + $event_id = isset($_POST['event_id']) ? absint($_POST['event_id']) : 0; + $attendee_ids = isset($_POST['attendee_ids']) && is_array($_POST['attendee_ids']) ? array_map('absint', $_POST['attendee_ids']) : array(); + $checked_in_only = isset($_POST['checked_in_only']) && $_POST['checked_in_only'] === 'yes'; + + if (!$event_id) { + wp_send_json_error(array('message' => 'Event ID is required')); + } + + if (empty($attendee_ids)) { + wp_send_json_error(array('message' => 'Please select at least one attendee')); + } + + // Check user permissions + $event = get_post($event_id); + if (!$event || !current_user_can('edit_post', $event->ID)) { + wp_send_json_error(array('message' => 'You do not have permission to generate certificates for this event')); + } + + // Load certificate generator + if (!class_exists('HVAC_Certificate_Generator')) { + require_once HVAC_PLUGIN_DIR . 'includes/certificates/class-certificate-generator.php'; + } + $certificate_generator = HVAC_Certificate_Generator::instance(); + + // Generate certificates in batch + $generation_results = $certificate_generator->generate_certificates_batch( + $event_id, + $attendee_ids, + array(), // Custom data (none for now) + get_current_user_id(), // Generated by current user + $checked_in_only // Only for checked-in attendees if selected + ); + + // Format response message + $message_parts = array(); + if ($generation_results['success'] > 0) { + $message_parts[] = sprintf('Successfully generated %d certificate(s).', $generation_results['success']); + } + + if ($generation_results['duplicate'] > 0) { + $message_parts[] = sprintf('%d duplicate(s) skipped.', $generation_results['duplicate']); + } + + if ($generation_results['not_checked_in'] > 0) { + $message_parts[] = sprintf('%d attendee(s) not checked in.', $generation_results['not_checked_in']); + } + + if ($generation_results['error'] > 0) { + $message_parts[] = sprintf('%d error(s).', $generation_results['error']); + } + + if ($generation_results['success'] > 0) { + // Generate preview URLs for the certificates just created + $preview_urls = array(); + if (!empty($generation_results['certificate_ids'])) { + foreach ($generation_results['certificate_ids'] as $certificate_id) { + $certificate = $this->certificate_manager->get_certificate($certificate_id); + if ($certificate && $certificate->file_path) { + // Generate secure download token for preview + $security = HVAC_Certificate_Security::instance(); + $preview_url = $security->generate_download_token($certificate_id, array( + 'file_path' => $certificate->file_path, + 'event_name' => get_the_title($certificate->event_id), + 'attendee_name' => $certificate->attendee_name + )); + if ($preview_url) { + $preview_urls[] = array( + 'certificate_id' => $certificate_id, + 'attendee_name' => $certificate->attendee_name, + 'preview_url' => $preview_url + ); + } + } + } + } + + wp_send_json_success(array( + 'message' => implode(' ', $message_parts), + 'results' => $generation_results, + 'preview_urls' => $preview_urls + )); + } else { + wp_send_json_error(array( + 'message' => 'Failed to generate certificates. ' . implode(' ', $message_parts), + 'results' => $generation_results + )); + } + } +} \ No newline at end of file diff --git a/includes/certificates/class-certificate-fix.php b/includes/certificates/class-certificate-fix.php new file mode 100644 index 00000000..fc698a8a --- /dev/null +++ b/includes/certificates/class-certificate-fix.php @@ -0,0 +1,59 @@ +You do not have permission to access this page.'; + } + + // Include the certificate fix template + ob_start(); + include HVAC_PLUGIN_DIR . 'templates/certificates/certificate-fix.php'; + return ob_get_clean(); + } +} + +// Initialize the class +HVAC_Certificate_Fix::instance(); \ No newline at end of file diff --git a/includes/certificates/class-certificate-generator.php b/includes/certificates/class-certificate-generator.php new file mode 100644 index 00000000..b0c10e02 --- /dev/null +++ b/includes/certificates/class-certificate-generator.php @@ -0,0 +1,833 @@ +certificate_manager = HVAC_Certificate_Manager::instance(); + } + + /** + * Generate a certificate for an attendee. + * + * @param int $event_id The event ID. + * @param int $attendee_id The attendee ID. + * @param array $custom_data Optional custom data to override defaults. + * @param int $generated_by The ID of the user who generated the certificate. + * + * @return int|false The certificate ID if successful, false otherwise. + */ + public function generate_certificate($event_id, $attendee_id, $custom_data = array(), $generated_by = 0) { + // Check if certificate already exists + if ($this->certificate_manager->certificate_exists($event_id, $attendee_id)) { + HVAC_Logger::warning("Certificate already exists for event $event_id and attendee $attendee_id", 'Certificates'); + return false; + } + + // Get attendee data + $attendee_data = $this->get_attendee_data($attendee_id); + + if (empty($attendee_data)) { + HVAC_Logger::error("Failed to retrieve attendee data for ID: $attendee_id", 'Certificates'); + return false; + } + + // Get event data + $event_data = $this->get_event_data($event_id); + + if (empty($event_data)) { + HVAC_Logger::error("Failed to retrieve event data for ID: $event_id", 'Certificates'); + return false; + } + + // Merge custom data + $certificate_data = array_merge($attendee_data, $event_data, $custom_data); + + // Create certificate record first + $user_id = $attendee_data['user_id'] ?? 0; + $certificate_id = $this->certificate_manager->create_certificate($event_id, $attendee_id, $user_id, '', $generated_by); + + if (!$certificate_id) { + HVAC_Logger::error("Failed to create certificate record for event $event_id and attendee $attendee_id", 'Certificates'); + return false; + } + + // Generate PDF and get file path + $file_path = $this->generate_pdf($certificate_id, $certificate_data); + + if (!$file_path) { + // Delete the certificate record if PDF generation failed + $this->certificate_manager->delete_certificate($certificate_id); + + HVAC_Logger::error("Failed to generate PDF for certificate ID: $certificate_id", 'Certificates'); + return false; + } + + // Generate PNG version for preview purposes + $png_path = $this->generate_png($certificate_id, $certificate_data); + if ($png_path) { + HVAC_Logger::info("Generated PNG version: $png_path", 'Certificates'); + } + + // Update certificate record with file paths + $this->certificate_manager->update_certificate_file($certificate_id, $file_path, $png_path); + + return $certificate_id; + } + + /** + * Generate a PDF certificate. + * + * @param int $certificate_id The certificate ID. + * @param array $certificate_data The certificate data. + * + * @return string|false The relative file path if successful, false otherwise. + */ + protected function generate_pdf($certificate_id, $certificate_data) { + // Get certificate and verify it exists + $certificate = $this->certificate_manager->get_certificate($certificate_id); + + if (!$certificate) { + return false; + } + + // Create a custom TCPDF class extension (for header/footer) + $pdf = $this->create_certificate_pdf(); + + // Add a page + $pdf->AddPage('L', 'LETTER'); // Landscape, Letter size + + // Set document metadata + $event_name = $certificate_data['event_name'] ?? 'HVAC Training'; + $attendee_name = $certificate_data['attendee_name'] ?? 'Attendee'; + + $pdf->SetCreator('HVAC Community Events'); + $pdf->SetAuthor('Upskill HVAC'); + $pdf->SetTitle("Certificate of Completion - $event_name"); + $pdf->SetSubject("Certificate for $attendee_name"); + $pdf->SetKeywords("HVAC, Certificate, Training, $event_name"); + + // Render certificate content + $this->render_certificate_content($pdf, $certificate, $certificate_data); + + // Get certificate storage path + $upload_dir = wp_upload_dir(); + $cert_dir = $upload_dir['basedir'] . '/' . get_option('hvac_certificate_storage_path', 'hvac-certificates'); + + // Create directory if it doesn't exist + if (!file_exists($cert_dir)) { + wp_mkdir_p($cert_dir); + } + + // Define file name and path + $file_name = sanitize_file_name( + 'certificate-' . $certificate->certificate_number . '-' . + sanitize_title($attendee_name) . '.pdf' + ); + + $event_dir = $cert_dir . '/' . $certificate->event_id; + + // Create event directory if it doesn't exist + if (!file_exists($event_dir)) { + wp_mkdir_p($event_dir); + } + + $full_path = $event_dir . '/' . $file_name; + $relative_path = get_option('hvac_certificate_storage_path', 'hvac-certificates') . + '/' . $certificate->event_id . '/' . $file_name; + + // Save the PDF file + try { + $pdf->Output($full_path, 'F'); // F means save to file + + if (file_exists($full_path)) { + return $relative_path; + } + } catch (Exception $e) { + HVAC_Logger::error("Failed to save PDF file: " . $e->getMessage(), 'Certificates'); + } + + return false; + } + + /** + * Generate a PNG version of the certificate for preview purposes. + * + * @param int $certificate_id The certificate ID. + * @param array $certificate_data The certificate data. + * + * @return string|false The relative file path if successful, false otherwise. + */ + protected function generate_png($certificate_id, $certificate_data) { + // Get certificate and verify it exists + $certificate = $this->certificate_manager->get_certificate($certificate_id); + + if (!$certificate) { + return false; + } + + // Create PDF for conversion to PNG + $pdf = $this->create_certificate_pdf(); + $pdf->AddPage(); + + // Render certificate content + $this->render_certificate_content($pdf, $certificate, $certificate_data); + + // Get certificate storage path + $upload_dir = wp_upload_dir(); + $cert_dir = $upload_dir['basedir'] . '/' . get_option('hvac_certificate_storage_path', 'hvac-certificates'); + + // Create directory if it doesn't exist + if (!file_exists($cert_dir)) { + wp_mkdir_p($cert_dir); + } + + $attendee_name = isset($certificate_data['attendee_name']) ? $certificate_data['attendee_name'] : 'unknown'; + + // Define file name and path + $file_name = sanitize_file_name( + 'certificate-' . $certificate->certificate_number . '-' . + sanitize_title($attendee_name) . '.png' + ); + + $event_dir = $cert_dir . '/' . $certificate->event_id; + + // Create event directory if it doesn't exist + if (!file_exists($event_dir)) { + wp_mkdir_p($event_dir); + } + + $full_path = $event_dir . '/' . $file_name; + $relative_path = get_option('hvac_certificate_storage_path', 'hvac-certificates') . + '/' . $certificate->event_id . '/' . $file_name; + + // Convert PDF to PNG using TCPDF's image output + try { + // Set high DPI for better quality + $pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); + + // Output as PNG (using TCPDF's built-in PNG output) + // Note: This requires TCPDF to be compiled with PNG support + $pdf->Output($full_path, 'F'); // Save PDF first + + // Convert PDF to PNG using ImageMagick if available + if (class_exists('Imagick')) { + $imagick = new Imagick(); + $imagick->setResolution(300, 300); // High resolution + $imagick->readImage($full_path); // Read the PDF + $imagick->setImageFormat('png'); + $imagick->setImageCompressionQuality(90); + + // Replace .pdf with .png in the path + $png_full_path = str_replace('.pdf', '.png', $full_path); + $png_relative_path = str_replace('.pdf', '.png', $relative_path); + + $imagick->writeImage($png_full_path); + $imagick->clear(); + + if (file_exists($png_full_path)) { + return $png_relative_path; + } + } else { + // Fallback: Log that ImageMagick is not available + HVAC_Logger::info("ImageMagick not available for PNG conversion. PNG generation skipped.", 'Certificates'); + return false; + } + + } catch (Exception $e) { + HVAC_Logger::error("Failed to generate PNG file: " . $e->getMessage(), 'Certificates'); + } + + return false; + } + + /** + * Create a TCPDF instance for certificate generation. + * + * @return TCPDF The TCPDF instance. + */ + protected function create_certificate_pdf() { + // Create new PDF document + $pdf = new TCPDF('L', 'mm', 'LETTER', true, 'UTF-8', false); + + // Set document information + $pdf->SetTitle('Certificate of Completion'); + $pdf->SetAuthor('Upskill HVAC'); + + // Set margins + $pdf->SetMargins(15, 15, 15); + + // Remove default header/footer + $pdf->setPrintHeader(false); + $pdf->setPrintFooter(false); + + // Set auto page breaks + $pdf->SetAutoPageBreak(false, 0); + + // Set default font + $pdf->SetFont('helvetica', '', 10); + + return $pdf; + } + + /** + * Render certificate content on PDF. + * + * @param TCPDF $pdf The TCPDF instance. + * @param object $certificate The certificate object. + * @param array $certificate_data The certificate data. + */ + protected function render_certificate_content($pdf, $certificate, $certificate_data) { + // Set background image if available + $this->add_certificate_background($pdf); + + // Add Upskill HVAC logo at the top + $this->add_upskill_logo($pdf); + + // Certificate title + $pdf->SetFont('helvetica', 'B', 30); + $pdf->SetTextColor(0, 77, 155); // Blue + $pdf->SetY(45); // Moved down to accommodate logo + $pdf->Cell(0, 20, 'CERTIFICATE OF COMPLETION', 0, 1, 'C'); + + // Description text + $pdf->SetFont('helvetica', '', 12); + $pdf->SetTextColor(77, 77, 77); // Dark gray + $pdf->SetY(70); + $pdf->Cell(0, 10, 'This certificate is awarded to', 0, 1, 'C'); + + // Attendee name - prominently displayed + $attendee_name = $certificate_data['attendee_name'] ?? 'Attendee Name'; + $pdf->SetFont('helvetica', 'B', 26); + $pdf->SetTextColor(0, 0, 0); // Black + $pdf->Cell(0, 20, $attendee_name, 0, 1, 'C'); + + // Course completion text + $pdf->SetFont('helvetica', '', 12); + $pdf->SetTextColor(77, 77, 77); // Dark gray + $pdf->Cell(0, 10, 'for successfully completing', 0, 1, 'C'); + + // Event name + $event_name = $certificate_data['event_name'] ?? 'HVAC Training Course'; + $pdf->SetFont('helvetica', 'B', 18); + $pdf->SetTextColor(0, 77, 155); // Blue + $pdf->Cell(0, 15, $event_name, 0, 1, 'C'); + + // Event date + $event_date = $certificate_data['event_date_formatted'] ?? date('F j, Y'); + $pdf->SetFont('helvetica', '', 12); + $pdf->SetTextColor(77, 77, 77); // Dark gray + $pdf->Cell(0, 10, 'on ' . $event_date, 0, 1, 'C'); + + // Get instructor/trainer name properly + $instructor_name = $certificate_data['instructor_name'] ?? ''; + if (empty($instructor_name) && !empty($certificate_data['trainer_name'])) { + $instructor_name = $certificate_data['trainer_name']; + } + if (empty($instructor_name)) { + // Try to get from event organizer + $instructor_name = $certificate_data['organization_name'] ?? 'Instructor'; + } + + // Draw a line for signature + $pdf->SetDrawColor(0, 77, 155); // Blue + $pdf->SetLineWidth(0.5); + $pdf->Line(70, 155, 190, 155); + + // Add instructor signature if available + if (!empty($certificate_data['instructor_signature'])) { + $signature_path = $certificate_data['instructor_signature']; + if (file_exists($signature_path)) { + $pdf->Image($signature_path, 110, 135, 40, 0, '', '', '', false, 300); + } + } + + // Instructor name and title + $pdf->SetY(160); + $pdf->SetFont('helvetica', 'B', 14); + $pdf->SetTextColor(0, 0, 0); // Black + $pdf->Cell(0, 10, $instructor_name, 0, 1, 'C'); + + $pdf->SetFont('helvetica', '', 11); + $pdf->SetTextColor(77, 77, 77); // Dark gray + $pdf->Cell(0, 8, 'Instructor / Trainer', 0, 1, 'C'); + + // Add organization name + $organization_name = 'Upskill HVAC'; + $pdf->SetY(180); + $pdf->SetFont('helvetica', 'B', 11); + $pdf->SetTextColor(0, 0, 0); // Black + $pdf->Cell(0, 8, $organization_name, 0, 1, 'C'); + + // Add venue info if available + $venue_name = $certificate_data['venue_name'] ?? ''; + if (!empty($venue_name)) { + $pdf->SetFont('helvetica', '', 10); + $pdf->SetTextColor(77, 77, 77); // Dark gray + $pdf->Cell(0, 8, $venue_name, 0, 1, 'C'); + } + + // Add certificate details at the bottom + $pdf->SetFont('helvetica', '', 8); + $pdf->SetTextColor(128, 128, 128); // Light gray + $pdf->SetY(195); + $pdf->Cell(0, 10, 'Certificate #: ' . $certificate->certificate_number . ' | Issue Date: ' . date('F j, Y', strtotime($certificate->date_generated)), 0, 1, 'C'); + + // Add decorative elements (optional) + $this->add_decorative_elements($pdf); + } + + /** + * Add certificate background. + * + * @param TCPDF $pdf The TCPDF instance. + */ + protected function add_certificate_background($pdf) { + // Check if custom background exists + $background_path = HVAC_PLUGIN_DIR . 'assets/images/certificate-background.jpg'; + + if (file_exists($background_path)) { + // Add background + $pdf->Image($background_path, 0, 0, $pdf->getPageWidth(), $pdf->getPageHeight(), '', '', '', false, 300); + } else { + // Create a simple background with border + $pdf->SetFillColor(255, 255, 255); + $pdf->Rect(0, 0, $pdf->getPageWidth(), $pdf->getPageHeight(), 'F'); + + // Add border + $pdf->SetDrawColor(0, 77, 155); // Blue + $pdf->SetLineWidth(1.5); + $pdf->Rect(5, 5, $pdf->getPageWidth() - 10, $pdf->getPageHeight() - 10, 'D'); + + // Add inner border + $pdf->SetDrawColor(200, 200, 200); // Light gray + $pdf->SetLineWidth(0.5); + $pdf->Rect(10, 10, $pdf->getPageWidth() - 20, $pdf->getPageHeight() - 20, 'D'); + } + } + + /** + * Add logo to certificate. + * + * @param TCPDF $pdf The TCPDF instance. + */ + protected function add_logo($pdf) { + // Check if logo exists + $logo_path = HVAC_PLUGIN_DIR . 'assets/images/certificate-logo.png'; + + if (file_exists($logo_path)) { + // Add logo at top left + $pdf->Image($logo_path, 15, 15, 40, 0, '', '', '', false, 300); + } + } + + /** + * Add Upskill HVAC logo to certificate. + * + * @param TCPDF $pdf The TCPDF instance. + */ + protected function add_upskill_logo($pdf) { + // Check for uploaded logo in WordPress uploads directory + $upload_dir = wp_upload_dir(); + $logo_path = $upload_dir['basedir'] . '/2025/05/UpskillHVAC-Logo_Black_NoOutline.png'; + + // Fallback to 2024 directory if 2025 doesn't exist + if (!file_exists($logo_path)) { + $logo_path = $upload_dir['basedir'] . '/2024/05/UpskillHVAC-Logo_Black_NoOutline.png'; + } + + // Check plugin assets directory as another fallback + if (!file_exists($logo_path)) { + $logo_path = HVAC_PLUGIN_DIR . 'assets/images/upskill-hvac-logo.png'; + } + + if (file_exists($logo_path)) { + // Add logo at top center + // Calculate center position - assuming letter size landscape (279.4mm wide) + $page_width = $pdf->getPageWidth(); + $logo_width = 50; // 50mm wide logo + $x_position = ($page_width - $logo_width) / 2; + + $pdf->Image($logo_path, $x_position, 10, $logo_width, 0, '', '', '', false, 300); + } else { + // If no logo found, add text branding + $pdf->SetFont('helvetica', 'B', 16); + $pdf->SetTextColor(0, 77, 155); // Blue + $pdf->SetY(15); + $pdf->Cell(0, 10, 'UPSKILL HVAC', 0, 1, 'C'); + } + } + + /** + * Add decorative elements to certificate. + * + * @param TCPDF $pdf The TCPDF instance. + */ + protected function add_decorative_elements($pdf) { + // Add decorative corner elements + $pdf->SetDrawColor(0, 77, 155); // Blue + $pdf->SetLineWidth(0.5); + + // Top left corner + $pdf->Line(10, 10, 30, 10); + $pdf->Line(10, 10, 10, 30); + + // Top right corner + $page_width = $pdf->getPageWidth(); + $pdf->Line($page_width - 30, 10, $page_width - 10, 10); + $pdf->Line($page_width - 10, 10, $page_width - 10, 30); + + // Bottom left corner + $page_height = $pdf->getPageHeight(); + $pdf->Line(10, $page_height - 30, 10, $page_height - 10); + $pdf->Line(10, $page_height - 10, 30, $page_height - 10); + + // Bottom right corner + $pdf->Line($page_width - 30, $page_height - 10, $page_width - 10, $page_height - 10); + $pdf->Line($page_width - 10, $page_height - 30, $page_width - 10, $page_height - 10); + } + + /** + * Get attendee data from Event Tickets. + * + * @param int $attendee_id The attendee ID. + * + * @return array Attendee data. + */ + protected function get_attendee_data($attendee_id) { + $attendee_data = array(); + + // Get attendee post + $attendee = get_post($attendee_id); + + if (!$attendee) { + return $attendee_data; + } + + // Try multiple meta keys for attendee name (matching the template query) + $meta_keys_for_name = array( + '_tec_tickets_commerce_full_name', // TEC Commerce + '_tribe_tpp_full_name', // Tribe PayPal Tickets + '_tribe_tickets_full_name', // Event Tickets + '_tribe_rsvp_full_name', // RSVP + 'attendee_full_name', // Legacy + '_name', // Generic + 'name' // Generic + ); + + $attendee_name = ''; + foreach ($meta_keys_for_name as $meta_key) { + $name = get_post_meta($attendee_id, $meta_key, true); + if (!empty($name)) { + $attendee_name = $name; + break; + } + } + + // If still no name, try first and last name separately + if (empty($attendee_name)) { + $first_name_keys = array( + '_tribe_tickets_first_name', + '_tribe_tpp_first_name', + '_tec_tickets_commerce_first_name', + 'attendee_first_name', + 'first_name' + ); + + $last_name_keys = array( + '_tribe_tickets_last_name', + '_tribe_tpp_last_name', + '_tec_tickets_commerce_last_name', + 'attendee_last_name', + 'last_name' + ); + + $first_name = ''; + $last_name = ''; + + foreach ($first_name_keys as $key) { + $fname = get_post_meta($attendee_id, $key, true); + if (!empty($fname)) { + $first_name = $fname; + break; + } + } + + foreach ($last_name_keys as $key) { + $lname = get_post_meta($attendee_id, $key, true); + if (!empty($lname)) { + $last_name = $lname; + break; + } + } + + if (!empty($first_name) || !empty($last_name)) { + $attendee_name = trim($first_name . ' ' . $last_name); + } + } + + // Try multiple meta keys for email (matching the template query) + $meta_keys_for_email = array( + '_tec_tickets_commerce_email', + '_tribe_tpp_email', + '_tribe_tickets_email', + '_tribe_tpp_attendee_email', + '_tribe_rsvp_email', + 'attendee_email', + '_email', + 'email' + ); + + $attendee_email = ''; + foreach ($meta_keys_for_email as $meta_key) { + $email = get_post_meta($attendee_id, $meta_key, true); + if (!empty($email) && is_email($email)) { + $attendee_email = $email; + break; + } + } + + // Try to find user by email + $user_id = 0; + if (!empty($attendee_email)) { + $user = get_user_by('email', $attendee_email); + if ($user) { + $user_id = $user->ID; + } + } + + // If still no name, use email prefix or "Attendee" + if (empty($attendee_name)) { + if (!empty($attendee_email)) { + $email_parts = explode('@', $attendee_email); + $attendee_name = ucwords(str_replace(array('.', '_', '-'), ' ', $email_parts[0])); + } else { + $attendee_name = 'Attendee #' . $attendee_id; + } + } + + // Build attendee data + $attendee_data = array( + 'attendee_id' => $attendee_id, + 'attendee_name' => $attendee_name, + 'attendee_email' => $attendee_email, + 'user_id' => $user_id + ); + + // Log for debugging + if (defined('WP_DEBUG') && WP_DEBUG && empty($attendee_name)) { + error_log("Certificate Generator: No name found for attendee ID $attendee_id"); + } + + return $attendee_data; + } + + /** + * Get event data from The Events Calendar. + * + * @param int $event_id The event ID. + * + * @return array Event data. + */ + protected function get_event_data($event_id) { + $event_data = array(); + + // Get event post + $event = get_post($event_id); + + if (!$event) { + return $event_data; + } + + // Get event details + $event_name = $event->post_title; + $event_date = tribe_get_start_date($event_id, false, 'F j, Y'); + + // Get venue details + $venue_id = tribe_get_venue_id($event_id); + $venue_name = tribe_get_venue($event_id); + + // Get organizer details + $organizer_id = tribe_get_organizer_id($event_id); + $organizer_name = tribe_get_organizer($event_id); + + // Get trainer/instructor name + // First check if this event has a trainer/author + $trainer_id = $event->post_author; + $trainer = get_userdata($trainer_id); + $instructor_name = ''; + + if ($trainer) { + // Try to get display name first + $instructor_name = $trainer->display_name; + + // If no display name, try full name + if (empty($instructor_name) || $instructor_name == $trainer->user_login) { + $first_name = get_user_meta($trainer_id, 'first_name', true); + $last_name = get_user_meta($trainer_id, 'last_name', true); + if ($first_name || $last_name) { + $instructor_name = trim($first_name . ' ' . $last_name); + } + } + + // Fallback to organizer name if still empty + if (empty($instructor_name)) { + $instructor_name = $organizer_name; + } + } else { + // Use organizer name as fallback + $instructor_name = $organizer_name; + } + + // Build event data + $event_data = array( + 'event_id' => $event_id, + 'event_name' => $event_name, + 'event_date' => get_post_meta($event_id, '_EventStartDate', true), + 'event_date_formatted' => $event_date, + 'venue_id' => $venue_id, + 'venue_name' => $venue_name, + 'organizer_id' => $organizer_id, + 'organization_name' => $organizer_name, + 'instructor_name' => $instructor_name, + 'trainer_name' => $instructor_name, // Also include as trainer_name for compatibility + 'trainer_id' => $trainer_id + ); + + return $event_data; + } + + /** + * Generate certificates in batch. + * + * @param int $event_id The event ID. + * @param array $attendee_ids Array of attendee IDs. + * @param array $custom_data Optional custom data to override defaults. + * @param int $generated_by The ID of the user who generated the certificates. + * @param bool $checked_in_only Whether to generate certificates only for checked-in attendees. + * + * @return array Results with success and error counts. + */ + public function generate_certificates_batch($event_id, $attendee_ids, $custom_data = array(), $generated_by = 0, $checked_in_only = false) { + $results = array( + 'success' => 0, + 'error' => 0, + 'duplicate' => 0, + 'not_checked_in' => 0, + 'certificate_ids' => array() + ); + + if (empty($attendee_ids) || !is_array($attendee_ids)) { + return $results; + } + + foreach ($attendee_ids as $attendee_id) { + // Check if certificate already exists + if ($this->certificate_manager->certificate_exists($event_id, $attendee_id)) { + $results['duplicate']++; + continue; + } + + // Check attendee check-in status if required + if ($checked_in_only) { + $is_checked_in = $this->is_attendee_checked_in($attendee_id); + + if (!$is_checked_in) { + $results['not_checked_in']++; + continue; + } + } + + $certificate_id = $this->generate_certificate($event_id, $attendee_id, $custom_data, $generated_by); + + if ($certificate_id) { + $results['success']++; + $results['certificate_ids'][] = $certificate_id; + } else { + $results['error']++; + } + } + + return $results; + } + + /** + * Check if an attendee is checked in. + * + * @param int $attendee_id The attendee ID. + * + * @return bool True if checked in, false otherwise. + */ + protected function is_attendee_checked_in($attendee_id) { + // Get attendee check-in status from Event Tickets + $check_in = get_post_meta($attendee_id, '_tribe_rsvp_checkedin', true); + + // For Event Tickets Plus we need to check a different meta key + if (empty($check_in)) { + $check_in = get_post_meta($attendee_id, '_tribe_tpp_checkedin', true); + } + + // If still empty, check the more general meta key + if (empty($check_in)) { + $check_in = get_post_meta($attendee_id, '_tribe_checkedin', true); + } + + return !empty($check_in) && $check_in == 1; + } +} \ No newline at end of file diff --git a/includes/certificates/class-certificate-installer.php b/includes/certificates/class-certificate-installer.php new file mode 100644 index 00000000..090174b8 --- /dev/null +++ b/includes/certificates/class-certificate-installer.php @@ -0,0 +1,195 @@ +get_charset_collate(); + $table_name = $wpdb->prefix . 'hvac_certificates'; + + // Create the certificates table + $sql = "CREATE TABLE $table_name ( + certificate_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + event_id BIGINT(20) UNSIGNED NOT NULL, + attendee_id BIGINT(20) UNSIGNED NOT NULL, + user_id BIGINT(20) UNSIGNED DEFAULT NULL, + certificate_number VARCHAR(50) NOT NULL, + file_path VARCHAR(255) NOT NULL, + png_path VARCHAR(255) DEFAULT NULL, + date_generated DATETIME NOT NULL, + generated_by BIGINT(20) UNSIGNED NOT NULL, + revoked TINYINT(1) NOT NULL DEFAULT 0, + revoked_date DATETIME DEFAULT NULL, + revoked_by BIGINT(20) UNSIGNED DEFAULT NULL, + revoked_reason TEXT DEFAULT NULL, + email_sent TINYINT(1) NOT NULL DEFAULT 0, + email_sent_date DATETIME DEFAULT NULL, + PRIMARY KEY (certificate_id), + UNIQUE KEY event_attendee (event_id, attendee_id), + KEY event_id (event_id), + KEY attendee_id (attendee_id), + KEY user_id (user_id), + KEY certificate_number (certificate_number), + KEY revoked (revoked) + ) $charset_collate;"; + + dbDelta($sql); + + // Set the version option + update_option('hvac_certificates_db_version', $this->db_version); + + // Create certificate options + if (false === get_option('hvac_certificate_counter')) { + add_option('hvac_certificate_counter', 0); + } + + if (false === get_option('hvac_certificate_prefix')) { + add_option('hvac_certificate_prefix', 'HVAC-'); + } + + if (false === get_option('hvac_certificate_storage_path')) { + // Default path is within wp-content/uploads/hvac-certificates + add_option('hvac_certificate_storage_path', 'hvac-certificates'); + } + + // Create the certificate storage directory + $this->create_certificates_directory(); + } + + /** + * Create certificates directory if it doesn't exist. + * + * @return bool True if directory exists or was created, false otherwise. + */ + public function create_certificates_directory() { + $upload_dir = wp_upload_dir(); + $cert_dir = $upload_dir['basedir'] . '/' . get_option('hvac_certificate_storage_path', 'hvac-certificates'); + + // Create directory if it doesn't exist + if (!file_exists($cert_dir)) { + wp_mkdir_p($cert_dir); + } + + // Create .htaccess file to protect directory + if (file_exists($cert_dir) && !file_exists($cert_dir . '/.htaccess')) { + $htaccess_content = "# Disable directory browsing +Options -Indexes + +# Deny access to php files + +Order Allow,Deny +Deny from all + + +# Allow PDF downloads only via WordPress + +Order Allow,Deny +Deny from all + + +# Restrict direct access + +RewriteEngine On +RewriteCond %{HTTP_REFERER} !^" . get_site_url() . " [NC] +RewriteRule \\.(pdf)$ - [NC,F,L] +"; + + file_put_contents($cert_dir . '/.htaccess', $htaccess_content); + } + + return file_exists($cert_dir); + } + + /** + * Check if the certificate tables exist and are up to date. + * + * @return bool True if tables are up to date, false otherwise. + */ + public function check_tables() { + global $wpdb; + + $installed_version = get_option('hvac_certificates_db_version'); + $table_name = $wpdb->prefix . 'hvac_certificates'; + + // Check if table exists + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name; + + // If table doesn't exist or version is different, create/update tables + if (!$table_exists || $installed_version !== $this->db_version) { + $this->create_tables(); + return false; + } + + return true; + } + + /** + * Upgrade routine for database tables. + * + * @return void + */ + public function maybe_upgrade() { + $installed_version = get_option('hvac_certificates_db_version'); + + // If installed version is different from current version, run upgrade + if ($installed_version !== $this->db_version) { + $this->create_tables(); + } + } +} \ No newline at end of file diff --git a/includes/certificates/class-certificate-manager-broken.php b/includes/certificates/class-certificate-manager-broken.php new file mode 100644 index 00000000..86e8f812 --- /dev/null +++ b/includes/certificates/class-certificate-manager-broken.php @@ -0,0 +1,1099 @@ +check_tables(); + } + + /** + * Generate a unique certificate number. + * + * @return string The generated certificate number. + */ + public function generate_certificate_number() { + $prefix = get_option('hvac_certificate_prefix', 'HVAC-'); + $counter = intval(get_option('hvac_certificate_counter', 0)); + + // Increment counter + $counter++; + update_option('hvac_certificate_counter', $counter); + + // Format: PREFIX-YEAR-SEQUENTIAL (e.g., HVAC-2023-00001) + $year = date('Y'); + $formatted_counter = str_pad($counter, 5, '0', STR_PAD_LEFT); + + return $prefix . $year . '-' . $formatted_counter; + } + + /** + * Creates a new certificate record in the database. + * + * @param int $event_id The event ID. + * @param int $attendee_id The attendee ID. + * @param int $user_id The associated user ID (if available). + * @param string $file_path The path to the certificate file. + * @param int $generated_by The ID of the user who generated the certificate. + * + * @return int|false The certificate ID if successful, false otherwise. + */ + public function create_certificate($event_id, $attendee_id, $user_id = 0, $file_path = '', $generated_by = 0) { + global $wpdb; + + // Get current user if not specified + if (empty($generated_by)) { + $generated_by = get_current_user_id(); + } + + // Generate certificate number + $certificate_number = $this->generate_certificate_number(); + + // Current date/time + $date_generated = current_time('mysql'); + + // Insert certificate record + $result = $wpdb->insert( + $wpdb->prefix . 'hvac_certificates', + array( + 'event_id' => $event_id, + 'attendee_id' => $attendee_id, + 'user_id' => $user_id, + 'certificate_number' => $certificate_number, + 'file_path' => $file_path, + 'date_generated' => $date_generated, + 'generated_by' => $generated_by, + 'revoked' => 0, + 'email_sent' => 0 + ), + array( + '%d', // event_id + '%d', // attendee_id + '%d', // user_id + '%s', // certificate_number + '%s', // file_path + '%s', // date_generated + '%d', // generated_by + '%d', // revoked + '%d' // email_sent + ) + ); + + if ($result) { + return $wpdb->insert_id; + } + + return false; + } + + /** + * Update the file paths for a certificate. + * + * @param int $certificate_id The certificate ID. + * @param string $file_path The PDF file path. + * @param string $png_path The PNG file path (optional). + * + * @return bool True if successful, false otherwise. + */ + public function update_certificate_file($certificate_id, $file_path, $png_path = null) { + global $wpdb; + + $update_data = array( + 'file_path' => $file_path + ); + $format = array('%s'); + + if ($png_path !== null) { + $update_data['png_path'] = $png_path; + $format[] = '%s'; + } + + $result = $wpdb->update( + $wpdb->prefix . 'hvac_certificates', + $update_data, + array( + 'certificate_id' => $certificate_id + ), + $format, + array('%d') + ); + + return $result !== false; + } + + /** + * Mark a certificate as sent via email. + * + * @param int $certificate_id The certificate ID. + * + * @return bool True if successful, false otherwise. + */ + public function mark_certificate_emailed($certificate_id) { + global $wpdb; + + $result = $wpdb->update( + $wpdb->prefix . 'hvac_certificates', + array( + 'email_sent' => 1, + 'email_sent_date' => current_time('mysql') + ), + array( + 'certificate_id' => $certificate_id + ), + array('%d', '%s'), + array('%d') + ); + + return $result !== false; + } + + /** + * Revoke a certificate. + * + * @param int $certificate_id The certificate ID. + * @param int $revoked_by The ID of the user who revoked the certificate. + * @param string $reason The reason for revocation. + * + * @return bool True if successful, false otherwise. + */ + public function revoke_certificate($certificate_id, $revoked_by = 0, $reason = '') { + global $wpdb; + + // Get current user if not specified + if (empty($revoked_by)) { + $revoked_by = get_current_user_id(); + } + + $result = $wpdb->update( + $wpdb->prefix . 'hvac_certificates', + array( + 'revoked' => 1, + 'revoked_date' => current_time('mysql'), + 'revoked_by' => $revoked_by, + 'revoked_reason' => $reason + ), + array( + 'certificate_id' => $certificate_id + ), + array('%d', '%s', '%d', '%s'), + array('%d') + ); + + return $result !== false; + } + + /** + * Get a certificate by ID. + * + * @param int $certificate_id The certificate ID. + * + * @return object|false The certificate object if found, false otherwise. + */ + public function get_certificate($certificate_id) { + global $wpdb; + + $query = $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}hvac_certificates WHERE certificate_id = %d", + $certificate_id + ); + + return $wpdb->get_row($query); + } + + /** + * Get a certificate by event ID and attendee ID. + * + * @param int $event_id The event ID. + * @param int $attendee_id The attendee ID. + * + * @return object|false The certificate object if found, false otherwise. + */ + public function get_certificate_by_attendee($event_id, $attendee_id) { + global $wpdb; + + $query = $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}hvac_certificates WHERE event_id = %d AND attendee_id = %d", + $event_id, $attendee_id + ); + + return $wpdb->get_row($query); + } + + /** + * Get all certificates for an event. + * + * @param int $event_id The event ID. + * @param bool $include_revoked Whether to include revoked certificates. + * + * @return array Array of certificate objects. + */ + public function get_certificates_by_event($event_id, $include_revoked = false) { + global $wpdb; + + $where = "WHERE event_id = %d"; + $params = array($event_id); + + if (!$include_revoked) { + $where .= " AND revoked = 0"; + } + + $query = $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}hvac_certificates $where ORDER BY date_generated DESC", + $params + ); + + return $wpdb->get_results($query); + } + + /** + * Get certificates count by event. + * + * @param int $event_id The event ID. + * + * @return array Certificate counts (total, active, revoked). + */ + public function get_certificates_count_by_event($event_id) { + global $wpdb; + + $query = $wpdb->prepare( + "SELECT + COUNT(*) as total, + SUM(CASE WHEN revoked = 0 THEN 1 ELSE 0 END) as active, + SUM(CASE WHEN revoked = 1 THEN 1 ELSE 0 END) as revoked, + SUM(CASE WHEN email_sent = 1 THEN 1 ELSE 0 END) as emailed + FROM {$wpdb->prefix}hvac_certificates + WHERE event_id = %d", + $event_id + ); + + $result = $wpdb->get_row($query); + + return array( + 'total' => intval($result->total), + 'active' => intval($result->active), + 'revoked' => intval($result->revoked), + 'emailed' => intval($result->emailed) + ); + } + + /** + * Get overall certificate statistics. + * + * @return array Certificate statistics. + */ + public function get_certificate_stats() { + global $wpdb; + + $query = "SELECT + COUNT(DISTINCT attendee_id) as total_trainees, + COUNT(DISTINCT event_id) as total_events_with_certificates, + COUNT(*) as total_certificates, + SUM(CASE WHEN revoked = 1 THEN 1 ELSE 0 END) as total_revoked, + SUM(CASE WHEN email_sent = 1 THEN 1 ELSE 0 END) as total_emailed + FROM {$wpdb->prefix}hvac_certificates"; + + $result = $wpdb->get_row($query); + + // Calculate average certificates per attendee + $avg_per_attendee = 0; + if (!empty($result->total_trainees)) { + $avg_per_attendee = $result->total_certificates / $result->total_trainees; + } + + return array( + 'total_trainees' => intval($result->total_trainees), + 'total_events' => intval($result->total_events_with_certificates), + 'total_certificates' => intval($result->total_certificates), + 'total_revoked' => intval($result->total_revoked), + 'total_emailed' => intval($result->total_emailed), + 'avg_per_attendee' => round($avg_per_attendee, 2) + ); + } + + /** + * Get all certificates for a specific attendee. + * + * @param int $attendee_id The attendee ID. + * @param bool $include_revoked Whether to include revoked certificates. + * + * @return array Array of certificate objects. + */ + public function get_certificates_by_attendee($attendee_id, $include_revoked = false) { + global $wpdb; + + $where = "WHERE attendee_id = %d"; + $params = array($attendee_id); + + if (!$include_revoked) { + $where .= " AND revoked = 0"; + } + + $query = $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}hvac_certificates $where ORDER BY date_generated DESC", + $params + ); + + return $wpdb->get_results($query); + } + + /** + * Get certificates by user ID. + * + * @param int $user_id The user ID. + * @param bool $include_revoked Whether to include revoked certificates. + * + * @return array Array of certificate objects. + */ + public function get_certificates_by_user($user_id, $include_revoked = false) { + global $wpdb; + + $where = "WHERE user_id = %d"; + $params = array($user_id); + + if (!$include_revoked) { + $where .= " AND revoked = 0"; + } + + $query = $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}hvac_certificates $where ORDER BY date_generated DESC", + $params + ); + + return $wpdb->get_results($query); + } + + /** + * Get all events that have certificates. + * + * @param int $user_id Optional user ID to filter events by author. + * @return array Array of event objects with certificate data. + */ + public function get_events_with_certificates($user_id = 0) { + global $wpdb; + + // Get events with certificates + $query = "SELECT + event_id, + COUNT(*) as total_certificates, + SUM(CASE WHEN revoked = 0 THEN 1 ELSE 0 END) as active_certificates, + SUM(CASE WHEN revoked = 1 THEN 1 ELSE 0 END) as revoked_certificates, + SUM(CASE WHEN email_sent = 1 THEN 1 ELSE 0 END) as emailed_certificates, + MAX(date_generated) as last_generated + FROM {$wpdb->prefix}hvac_certificates + GROUP BY event_id + ORDER BY last_generated DESC"; + + $certificate_data = $wpdb->get_results($query, OBJECT_K); + + // Get event data + $event_ids = array_keys($certificate_data); + + if (empty($event_ids)) { + return array(); + } + + // Build WP_Query args + $args = array( + 'post_type' => Tribe__Events__Main::POSTTYPE, + 'post__in' => $event_ids, + 'posts_per_page' => -1, + 'orderby' => 'post__in', + 'post_status' => 'publish' + ); + + // Filter by user if specified + if ($user_id > 0) { + $args['author'] = $user_id; + } + + $events = get_posts($args); + + return $events; + } + + /** + * Get certificates for events created by a specific user. + * + * @param int $user_id The user ID. + * @param array $args Additional query args (limit, offset, etc.). + * + * @return array Array of certificate objects. + */ + public function get_user_certificates($user_id, $args = array()) { + global $wpdb; + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('get_user_certificates called with user_id', $user_id); + hvac_debug_log('get_user_certificates args', $args); + } + + $defaults = array( + 'page' => 1, + 'per_page' => 20, + 'orderby' => 'date_generated', + 'order' => 'DESC', + 'event_id' => 0, + 'revoked' => null, + 'limit' => 0, + 'search_attendee' => '' + ); + + $args = wp_parse_args($args, $defaults); + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Args after parsing defaults', $args); + } + + // Build WHERE clause + $where = array(); + $where_values = array(); + + // Get event IDs authored by this user + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Creating WP_Query to get user events'); + } + + try { + // Use direct database query to get user's event IDs (bypassing TEC interference) + $event_ids = $wpdb->get_col($wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} + WHERE post_type = %s + AND post_author = %d + AND post_status = 'publish'", + 'tribe_events', + $user_id + )); + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Direct DB query completed, event_ids count', count($event_ids)); + } + + if (empty($event_ids)) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('No events found for user, returning empty array'); + } + return array(); + } + + // Filter by event ID if specified + if (!empty($args['event_id'])) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Filter by specific event ID', $args['event_id']); + } + + // Check if the specified event belongs to the user + if (in_array($args['event_id'], $event_ids)) { + $where[] = "event_id = %d"; + $where_values[] = $args['event_id']; + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Event belongs to user, adding to WHERE clause'); + } + } else { + // Event doesn't belong to this user + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Event does not belong to user, returning empty array'); + } + return array(); + } + } else { + // Include all user's events + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Including all user events in query'); + } + + $event_ids_string = implode(',', array_map('intval', $event_ids)); + + // Check if we have a valid string of event IDs + if (empty($event_ids_string)) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Empty event_ids_string, returning empty array'); + } + return array(); + } + + $where[] = "event_id IN ($event_ids_string)"; + } + + // Filter by revocation status if specified + if (isset($args['revoked']) && $args['revoked'] !== null) { + $where[] = "revoked = %d"; + $where_values[] = (int) $args['revoked']; + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Added revoked filter', $args['revoked']); + } + } + + // Build WHERE clause + $where_clause = !empty($where) ? "WHERE " . implode(" AND ", $where) : ""; + + // Build ORDER BY clause + $order_by = sanitize_sql_orderby($args['orderby'] . ' ' . $args['order']); + + // Build LIMIT clause + $limit_clause = ''; + if ($args['limit'] > 0) { + $limit_clause = "LIMIT %d"; + $where_values[] = $args['limit']; + } elseif ($args['per_page'] > 0) { + $offset = ($args['page'] - 1) * $args['per_page']; + $limit_clause = "LIMIT %d, %d"; + $where_values[] = $offset; + $where_values[] = $args['per_page']; + } + + // Check if the table exists before querying + $table_name = $wpdb->prefix . 'hvac_certificates'; + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name; + + if (!$table_exists) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Table does not exist: ' . $table_name); + } + return array(); + } + + // Add WHERE clause for attendee search if provided + if (!empty($args['search_attendee'])) { + $search_term = '%' . $wpdb->esc_like($args['search_attendee']) . '%'; + + if (empty($where)) { + $where[] = "( + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_full_name' AND pm.meta_value LIKE %s + ) + OR + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_email' AND pm.meta_value LIKE %s + ) + )"; + $where_values[] = $search_term; + $where_values[] = $search_term; + } else { + $where[] = "AND ( + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_full_name' AND pm.meta_value LIKE %s + ) + OR + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_email' AND pm.meta_value LIKE %s + ) + )"; + $where_values[] = $search_term; + $where_values[] = $search_term; + } + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Added attendee search filter', $args['search_attendee']); + } + } + + // Build WHERE clause + $where_clause = !empty($where) ? "WHERE " . implode(" ", $where) : ""; + + // Build final query + $query = "SELECT * FROM {$wpdb->prefix}hvac_certificates $where_clause ORDER BY $order_by $limit_clause"; + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Final query before prepare', $query); + hvac_debug_log('Where values', $where_values); + } + + // Prepare the query if we have where values + if (!empty($where_values)) { + $query = $wpdb->prepare($query, $where_values); + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Prepared query', $query); + } + } + + $results = $wpdb->get_results($query); + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Query executed, results count', is_array($results) ? count($results) : 'null'); + if ($wpdb->last_error) { + hvac_debug_log('Database error', $wpdb->last_error); + } + } + + return $results; + + } catch (Exception $e) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Exception in get_user_certificates', $e->getMessage()); + } + return array(); + } + } + + /** + * Get the total count of certificates for a specific user. + * + * @param int $user_id The user ID. + * @param array $args Additional query args. + * + * @return int Total count of certificates. + */ + public function get_user_certificate_count($user_id, $args = array()) { + global $wpdb; + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('get_user_certificate_count called with user_id', $user_id); + hvac_debug_log('get_user_certificate_count args', $args); + } + + try { + // Use direct database query to get user's event IDs (bypassing TEC interference) + $event_ids = $wpdb->get_col($wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} + WHERE post_type = %s + AND post_author = %d + AND post_status = 'publish'", + 'tribe_events', + $user_id + )); + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Direct DB query for events completed, count', count($event_ids)); + } + + if (empty($event_ids)) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('No events found for user, returning 0'); + } + return 0; + } + + // Build WHERE clause + $where = array(); + $where_values = array(); + + // Filter by event ID if specified + if (!empty($args['event_id'])) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Filter by event ID', $args['event_id']); + } + + // Check if the specified event belongs to the user + if (in_array($args['event_id'], $event_ids)) { + $where[] = "event_id = %d"; + $where_values[] = $args['event_id']; + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Event belongs to user, adding to WHERE clause'); + } + } else { + // Event doesn't belong to this user + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Event does not belong to user, returning 0'); + } + return 0; + } + } else { + // Include all user's events + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Including all user events in query'); + } + + $event_ids_string = implode(',', array_map('intval', $event_ids)); + + // Make sure we have event IDs + if (empty($event_ids_string)) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Empty event_ids_string, returning 0'); + } + return 0; + } + + $where[] = "event_id IN ($event_ids_string)"; + } + + // Filter by revocation status if specified + if (isset($args['revoked']) && $args['revoked'] !== null) { + $where[] = "revoked = %d"; + $where_values[] = (int) $args['revoked']; + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Added revoked filter', $args['revoked']); + } + } + + // Check if table exists + $table_name = $wpdb->prefix . 'hvac_certificates'; + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name; + + if (!$table_exists) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Table does not exist: ' . $table_name); + } + return 0; + } + + // Build WHERE clause + $where_clause = !empty($where) ? "WHERE " . implode(" AND ", $where) : ""; + + // Add WHERE clause for attendee search if provided + if (!empty($args['search_attendee'])) { + $search_term = '%' . $wpdb->esc_like($args['search_attendee']) . '%'; + + if (empty($where)) { + $where[] = "( + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_full_name' AND pm.meta_value LIKE %s + ) + OR + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_email' AND pm.meta_value LIKE %s + ) + )"; + $where_values[] = $search_term; + $where_values[] = $search_term; + } else { + $where[] = "AND ( + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_full_name' AND pm.meta_value LIKE %s + ) + OR + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_email' AND pm.meta_value LIKE %s + ) + )"; + $where_values[] = $search_term; + $where_values[] = $search_term; + } + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Added attendee search filter to count query', $args['search_attendee']); + } + } + + // Build WHERE clause + $where_clause = !empty($where) ? "WHERE " . implode(" ", $where) : ""; + + // Build final query + $query = "SELECT COUNT(*) FROM {$wpdb->prefix}hvac_certificates $where_clause"; + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Final query before prepare', $query); + } + + // Prepare the query if we have where values + if (!empty($where_values)) { + $query = $wpdb->prepare($query, $where_values); + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Prepared query', $query); + } + } + + $count = $wpdb->get_var($query); + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Query executed, count result', $count); + if ($wpdb->last_error) { + hvac_debug_log('Database error', $wpdb->last_error); + } + } + + return intval($count); + + } catch (Exception $e) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Exception in get_user_certificate_count', $e->getMessage()); + } + return 0; + } + } + + /** + * Get certificate statistics for a specific user. + * + * @param int $user_id The user ID. + * + * @return array Certificate statistics. + */ + public function get_user_certificate_stats($user_id) { + global $wpdb; + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('get_user_certificate_stats called with user_id', $user_id); + } + + // Default empty stats + $empty_stats = array( + 'total' => 0, + 'active' => 0, + 'revoked' => 0, + 'emailed' => 0 + ); + + try { + // Check if table exists before querying + $table_name = $wpdb->prefix . 'hvac_certificates'; + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name; + + if (!$table_exists) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Table does not exist: ' . $table_name); + } + return $empty_stats; + } + + // Use direct database query to get user's event IDs (bypassing TEC interference) + $event_ids = $wpdb->get_col($wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} + WHERE post_type = %s + AND post_author = %d + AND post_status = 'publish'", + 'tribe_events', + $user_id + )); + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Direct DB query for events in stats, count', count($event_ids)); + } + + if (empty($event_ids)) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('No events found for user in stats, returning empty stats'); + } + return $empty_stats; + } + + // Create string of event IDs for query + $event_ids_string = implode(',', array_map('intval', $event_ids)); + + if (empty($event_ids_string)) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Empty event_ids_string, returning empty stats'); + } + return $empty_stats; + } + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Building statistics query for events', $event_ids_string); + } + + $query = "SELECT + COUNT(*) as total, + SUM(CASE WHEN revoked = 0 THEN 1 ELSE 0 END) as active, + SUM(CASE WHEN revoked = 1 THEN 1 ELSE 0 END) as revoked, + SUM(CASE WHEN email_sent = 1 THEN 1 ELSE 0 END) as emailed + FROM {$wpdb->prefix}hvac_certificates + WHERE event_id IN ($event_ids_string)"; + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Statistics query', $query); + } + + $result = $wpdb->get_row($query); + + if ($wpdb->last_error) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Database error in get_user_certificate_stats', $wpdb->last_error); + } + return $empty_stats; + } + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Query executed, result', $result); + } + + if (is_null($result)) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Null result returned, using empty stats'); + } + return $empty_stats; + } + + $stats = array( + 'total' => intval($result->total), + 'active' => intval($result->active), + 'revoked' => intval($result->revoked), + 'emailed' => intval($result->emailed) + ); + + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Returning stats', $stats); + } + + return $stats; + } catch (Exception $e) { + if (function_exists('hvac_debug_log')) { + hvac_debug_log('Exception in get_user_certificate_stats', $e->getMessage()); + } + return $empty_stats; + } + } + + /** + * Get certificate file path. + * + * @param int $certificate_id The certificate ID. + * + * @return string|false The file path if found, false otherwise. + */ + public function get_certificate_file_path($certificate_id) { + $certificate = $this->get_certificate($certificate_id); + + if (!$certificate) { + return false; + } + + // Get uploads directory + $upload_dir = wp_upload_dir(); + $base_dir = $upload_dir['basedir']; + + // Construct full path + $full_path = $base_dir . '/' . $certificate->file_path; + + if (file_exists($full_path)) { + return $full_path; + } + + return false; + } + + /** + * Get certificate file URL. + * + * @param int $certificate_id The certificate ID. + * + * @return string|false The file URL if found, false otherwise. + */ + public function get_certificate_url($certificate_id) { + // Create a secure URL with nonce for downloading + $url = add_query_arg( + array( + 'action' => 'hvac_download_certificate', + 'certificate_id' => $certificate_id, + 'nonce' => wp_create_nonce('download_certificate_' . $certificate_id) + ), + admin_url('admin-ajax.php') + ); + + return $url; + } + + /** + * Check if an attendee already has a certificate for an event. + * + * @param int $event_id The event ID. + * @param int $attendee_id The attendee ID. + * + * @return bool True if a certificate exists, false otherwise. + */ + public function certificate_exists($event_id, $attendee_id) { + $certificate = $this->get_certificate_by_attendee($event_id, $attendee_id); + return !empty($certificate); + } + + /** + * Delete a certificate record and its file. + * + * @param int $certificate_id The certificate ID. + * + * @return bool True if successful, false otherwise. + */ + public function delete_certificate($certificate_id) { + global $wpdb; + + // Get certificate to get file path + $certificate = $this->get_certificate($certificate_id); + + if (!$certificate) { + return false; + } + + // Delete file if it exists + if (!empty($certificate->file_path)) { + $upload_dir = wp_upload_dir(); + $full_path = $upload_dir['basedir'] . '/' . $certificate->file_path; + + if (file_exists($full_path)) { + unlink($full_path); + } + } + + // Delete from database + $result = $wpdb->delete( + $wpdb->prefix . 'hvac_certificates', + array('certificate_id' => $certificate_id), + array('%d') + ); + + return $result !== false; + } +} \ No newline at end of file diff --git a/includes/certificates/class-certificate-manager.php b/includes/certificates/class-certificate-manager.php new file mode 100644 index 00000000..fd9d75dd --- /dev/null +++ b/includes/certificates/class-certificate-manager.php @@ -0,0 +1,906 @@ +check_tables(); + } + + /** + * Generate a unique certificate number. + * + * @return string The generated certificate number. + */ + public function generate_certificate_number() { + $prefix = get_option('hvac_certificate_prefix', 'HVAC-'); + $counter = intval(get_option('hvac_certificate_counter', 0)); + + // Increment counter + $counter++; + update_option('hvac_certificate_counter', $counter); + + // Format: PREFIX-YEAR-SEQUENTIAL (e.g., HVAC-2023-00001) + $year = date('Y'); + $formatted_counter = str_pad($counter, 5, '0', STR_PAD_LEFT); + + return $prefix . $year . '-' . $formatted_counter; + } + + /** + * Creates a new certificate record in the database. + * + * @param int $event_id The event ID. + * @param int $attendee_id The attendee ID. + * @param int $user_id The associated user ID (if available). + * @param string $file_path The path to the certificate file. + * @param int $generated_by The ID of the user who generated the certificate. + * + * @return int|false The certificate ID if successful, false otherwise. + */ + public function create_certificate($event_id, $attendee_id, $user_id = 0, $file_path = '', $generated_by = 0) { + global $wpdb; + + // Get current user if not specified + if (empty($generated_by)) { + $generated_by = get_current_user_id(); + } + + // Generate certificate number + $certificate_number = $this->generate_certificate_number(); + + // Current date/time + $date_generated = current_time('mysql'); + + // Insert certificate record + $result = $wpdb->insert( + $wpdb->prefix . 'hvac_certificates', + array( + 'event_id' => $event_id, + 'attendee_id' => $attendee_id, + 'user_id' => $user_id, + 'certificate_number' => $certificate_number, + 'file_path' => $file_path, + 'date_generated' => $date_generated, + 'generated_by' => $generated_by, + 'revoked' => 0, + 'email_sent' => 0 + ), + array( + '%d', // event_id + '%d', // attendee_id + '%d', // user_id + '%s', // certificate_number + '%s', // file_path + '%s', // date_generated + '%d', // generated_by + '%d', // revoked + '%d' // email_sent + ) + ); + + if ($result) { + return $wpdb->insert_id; + } + + return false; + } + + /** + * Update the file paths for a certificate. + * + * @param int $certificate_id The certificate ID. + * @param string $file_path The PDF file path. + * @param string $png_path The PNG file path (optional). + * + * @return bool True if successful, false otherwise. + */ + public function update_certificate_file($certificate_id, $file_path, $png_path = null) { + global $wpdb; + + $update_data = array( + 'file_path' => $file_path + ); + $format = array('%s'); + + if ($png_path !== null) { + $update_data['png_path'] = $png_path; + $format[] = '%s'; + } + + $result = $wpdb->update( + $wpdb->prefix . 'hvac_certificates', + $update_data, + array( + 'certificate_id' => $certificate_id + ), + $format, + array('%d') + ); + + return $result !== false; + } + + /** + * Mark a certificate as sent via email. + * + * @param int $certificate_id The certificate ID. + * + * @return bool True if successful, false otherwise. + */ + public function mark_certificate_emailed($certificate_id) { + global $wpdb; + + $result = $wpdb->update( + $wpdb->prefix . 'hvac_certificates', + array( + 'email_sent' => 1, + 'email_sent_date' => current_time('mysql') + ), + array( + 'certificate_id' => $certificate_id + ), + array('%d', '%s'), + array('%d') + ); + + return $result !== false; + } + + /** + * Revoke a certificate. + * + * @param int $certificate_id The certificate ID. + * @param int $revoked_by The ID of the user who revoked the certificate. + * @param string $reason The reason for revocation. + * + * @return bool True if successful, false otherwise. + */ + public function revoke_certificate($certificate_id, $revoked_by = 0, $reason = '') { + global $wpdb; + + // Get current user if not specified + if (empty($revoked_by)) { + $revoked_by = get_current_user_id(); + } + + $result = $wpdb->update( + $wpdb->prefix . 'hvac_certificates', + array( + 'revoked' => 1, + 'revoked_date' => current_time('mysql'), + 'revoked_by' => $revoked_by, + 'revoked_reason' => $reason + ), + array( + 'certificate_id' => $certificate_id + ), + array('%d', '%s', '%d', '%s'), + array('%d') + ); + + return $result !== false; + } + + /** + * Get a certificate by ID. + * + * @param int $certificate_id The certificate ID. + * + * @return object|false The certificate object if found, false otherwise. + */ + public function get_certificate($certificate_id) { + global $wpdb; + + $query = $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}hvac_certificates WHERE certificate_id = %d", + $certificate_id + ); + + return $wpdb->get_row($query); + } + + /** + * Get a certificate by event ID and attendee ID. + * + * @param int $event_id The event ID. + * @param int $attendee_id The attendee ID. + * + * @return object|false The certificate object if found, false otherwise. + */ + public function get_certificate_by_attendee($event_id, $attendee_id) { + global $wpdb; + + $query = $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}hvac_certificates WHERE event_id = %d AND attendee_id = %d", + $event_id, $attendee_id + ); + + return $wpdb->get_row($query); + } + + /** + * Get all certificates for an event. + * + * @param int $event_id The event ID. + * @param bool $include_revoked Whether to include revoked certificates. + * + * @return array Array of certificate objects. + */ + public function get_certificates_by_event($event_id, $include_revoked = false) { + global $wpdb; + + $where = "WHERE event_id = %d"; + $params = array($event_id); + + if (!$include_revoked) { + $where .= " AND revoked = 0"; + } + + $query = $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}hvac_certificates $where ORDER BY date_generated DESC", + $params + ); + + return $wpdb->get_results($query); + } + + /** + * Get certificates count by event. + * + * @param int $event_id The event ID. + * + * @return array Certificate counts (total, active, revoked). + */ + public function get_certificates_count_by_event($event_id) { + global $wpdb; + + $query = $wpdb->prepare( + "SELECT + COUNT(*) as total, + SUM(CASE WHEN revoked = 0 THEN 1 ELSE 0 END) as active, + SUM(CASE WHEN revoked = 1 THEN 1 ELSE 0 END) as revoked, + SUM(CASE WHEN email_sent = 1 THEN 1 ELSE 0 END) as emailed + FROM {$wpdb->prefix}hvac_certificates + WHERE event_id = %d", + $event_id + ); + + $result = $wpdb->get_row($query); + + return array( + 'total' => intval($result->total), + 'active' => intval($result->active), + 'revoked' => intval($result->revoked), + 'emailed' => intval($result->emailed) + ); + } + + /** + * Get overall certificate statistics. + * + * @return array Certificate statistics. + */ + public function get_certificate_stats() { + global $wpdb; + + $query = "SELECT + COUNT(DISTINCT attendee_id) as total_trainees, + COUNT(DISTINCT event_id) as total_events_with_certificates, + COUNT(*) as total_certificates, + SUM(CASE WHEN revoked = 1 THEN 1 ELSE 0 END) as total_revoked, + SUM(CASE WHEN email_sent = 1 THEN 1 ELSE 0 END) as total_emailed + FROM {$wpdb->prefix}hvac_certificates"; + + $result = $wpdb->get_row($query); + + // Calculate average certificates per attendee + $avg_per_attendee = 0; + if (!empty($result->total_trainees)) { + $avg_per_attendee = $result->total_certificates / $result->total_trainees; + } + + return array( + 'total_trainees' => intval($result->total_trainees), + 'total_events' => intval($result->total_events_with_certificates), + 'total_certificates' => intval($result->total_certificates), + 'total_revoked' => intval($result->total_revoked), + 'total_emailed' => intval($result->total_emailed), + 'avg_per_attendee' => round($avg_per_attendee, 2) + ); + } + + /** + * Get all certificates for a specific attendee. + * + * @param int $attendee_id The attendee ID. + * @param bool $include_revoked Whether to include revoked certificates. + * + * @return array Array of certificate objects. + */ + public function get_certificates_by_attendee($attendee_id, $include_revoked = false) { + global $wpdb; + + $where = "WHERE attendee_id = %d"; + $params = array($attendee_id); + + if (!$include_revoked) { + $where .= " AND revoked = 0"; + } + + $query = $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}hvac_certificates $where ORDER BY date_generated DESC", + $params + ); + + return $wpdb->get_results($query); + } + + /** + * Get certificates by user ID. + * + * @param int $user_id The user ID. + * @param bool $include_revoked Whether to include revoked certificates. + * + * @return array Array of certificate objects. + */ + public function get_certificates_by_user($user_id, $include_revoked = false) { + global $wpdb; + + $where = "WHERE user_id = %d"; + $params = array($user_id); + + if (!$include_revoked) { + $where .= " AND revoked = 0"; + } + + $query = $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}hvac_certificates $where ORDER BY date_generated DESC", + $params + ); + + return $wpdb->get_results($query); + } + + /** + * Get all events that have certificates. + * + * @param int $user_id Optional user ID to filter events by author. + * @return array Array of event objects with certificate data. + */ + public function get_events_with_certificates($user_id = 0) { + global $wpdb; + + // Get events with certificates + $query = "SELECT + event_id, + COUNT(*) as total_certificates, + SUM(CASE WHEN revoked = 0 THEN 1 ELSE 0 END) as active_certificates, + SUM(CASE WHEN revoked = 1 THEN 1 ELSE 0 END) as revoked_certificates, + SUM(CASE WHEN email_sent = 1 THEN 1 ELSE 0 END) as emailed_certificates, + MAX(date_generated) as last_generated + FROM {$wpdb->prefix}hvac_certificates + GROUP BY event_id + ORDER BY last_generated DESC"; + + $certificate_data = $wpdb->get_results($query, OBJECT_K); + + // Get event data + $event_ids = array_keys($certificate_data); + + if (empty($event_ids)) { + return array(); + } + + // Build WP_Query args + $args = array( + 'post_type' => Tribe__Events__Main::POSTTYPE, + 'post__in' => $event_ids, + 'posts_per_page' => -1, + 'orderby' => 'post__in', + 'post_status' => 'publish' + ); + + // Filter by user if specified + if ($user_id > 0) { + $args['author'] = $user_id; + } + + $events = get_posts($args); + + return $events; + } + + /** + * Get certificates for events created by a specific user. + * + * @param int $user_id The user ID. + * @param array $args Additional query args (limit, offset, etc.). + * + * @return array Array of certificate objects. + */ + public function get_user_certificates($user_id, $args = array()) { + global $wpdb; + + $defaults = array( + 'page' => 1, + 'per_page' => 20, + 'orderby' => 'date_generated', + 'order' => 'DESC', + 'event_id' => 0, + 'revoked' => null, + 'limit' => 0, + 'search_attendee' => '' + ); + + $args = wp_parse_args($args, $defaults); + + // Build WHERE clause + $where = array(); + $where_values = array(); + + try { + // Use direct database query to get user's event IDs (bypassing TEC interference) + $event_ids = $wpdb->get_col($wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} + WHERE post_type = %s + AND post_author = %d + AND post_status = 'publish'", + 'tribe_events', + $user_id + )); + + if (empty($event_ids)) { + return array(); + } + + // Filter by event ID if specified + if (!empty($args['event_id'])) { + // Check if the specified event belongs to the user + if (in_array($args['event_id'], $event_ids)) { + $where[] = "event_id = %d"; + $where_values[] = $args['event_id']; + } else { + // Event doesn't belong to this user + return array(); + } + } else { + // Include all user's events + $event_ids_string = implode(',', array_map('intval', $event_ids)); + + // Check if we have a valid string of event IDs + if (empty($event_ids_string)) { + return array(); + } + + $where[] = "event_id IN ($event_ids_string)"; + } + + // Filter by revocation status if specified + if (isset($args['revoked']) && $args['revoked'] !== null) { + $where[] = "revoked = %d"; + $where_values[] = (int) $args['revoked']; + } + + // Build WHERE clause + $where_clause = !empty($where) ? "WHERE " . implode(" AND ", $where) : ""; + + // Build ORDER BY clause + $order_by = sanitize_sql_orderby($args['orderby'] . ' ' . $args['order']); + + // Build LIMIT clause + $limit_clause = ''; + if ($args['limit'] > 0) { + $limit_clause = "LIMIT %d"; + $where_values[] = $args['limit']; + } elseif ($args['per_page'] > 0) { + $offset = ($args['page'] - 1) * $args['per_page']; + $limit_clause = "LIMIT %d, %d"; + $where_values[] = $offset; + $where_values[] = $args['per_page']; + } + + // Check if the table exists before querying + $table_name = $wpdb->prefix . 'hvac_certificates'; + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name; + + if (!$table_exists) { + return array(); + } + + // Add WHERE clause for attendee search if provided + if (!empty($args['search_attendee'])) { + $search_term = '%' . $wpdb->esc_like($args['search_attendee']) . '%'; + + if (empty($where)) { + $where[] = "( + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_full_name' AND pm.meta_value LIKE %s + ) + OR + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_email' AND pm.meta_value LIKE %s + ) + )"; + $where_values[] = $search_term; + $where_values[] = $search_term; + } else { + $where[] = "AND ( + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_full_name' AND pm.meta_value LIKE %s + ) + OR + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_email' AND pm.meta_value LIKE %s + ) + )"; + $where_values[] = $search_term; + $where_values[] = $search_term; + } + } + + // Build WHERE clause + $where_clause = !empty($where) ? "WHERE " . implode(" ", $where) : ""; + + // Build final query + $query = "SELECT * FROM {$wpdb->prefix}hvac_certificates $where_clause ORDER BY $order_by $limit_clause"; + + // Prepare the query if we have where values + if (!empty($where_values)) { + $query = $wpdb->prepare($query, $where_values); + } + + $results = $wpdb->get_results($query); + + return $results; + + } catch (Exception $e) { + return array(); + } + } + + /** + * Get the total count of certificates for a specific user. + * + * @param int $user_id The user ID. + * @param array $args Additional query args. + * + * @return int Total count of certificates. + */ + public function get_user_certificate_count($user_id, $args = array()) { + global $wpdb; + + try { + // Use direct database query to get user's event IDs (bypassing TEC interference) + $event_ids = $wpdb->get_col($wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} + WHERE post_type = %s + AND post_author = %d + AND post_status = 'publish'", + 'tribe_events', + $user_id + )); + + if (empty($event_ids)) { + return 0; + } + + // Build WHERE clause + $where = array(); + $where_values = array(); + + // Filter by event ID if specified + if (!empty($args['event_id'])) { + // Check if the specified event belongs to the user + if (in_array($args['event_id'], $event_ids)) { + $where[] = "event_id = %d"; + $where_values[] = $args['event_id']; + } else { + // Event doesn't belong to this user + return 0; + } + } else { + // Include all user's events + $event_ids_string = implode(',', array_map('intval', $event_ids)); + + // Make sure we have event IDs + if (empty($event_ids_string)) { + return 0; + } + + $where[] = "event_id IN ($event_ids_string)"; + } + + // Filter by revocation status if specified + if (isset($args['revoked']) && $args['revoked'] !== null) { + $where[] = "revoked = %d"; + $where_values[] = (int) $args['revoked']; + } + + // Check if table exists + $table_name = $wpdb->prefix . 'hvac_certificates'; + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name; + + if (!$table_exists) { + return 0; + } + + // Add WHERE clause for attendee search if provided + if (!empty($args['search_attendee'])) { + $search_term = '%' . $wpdb->esc_like($args['search_attendee']) . '%'; + + $where[] = "( + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_full_name' AND pm.meta_value LIKE %s + ) + OR + certificate_id IN ( + SELECT c.certificate_id + FROM {$wpdb->prefix}hvac_certificates c + JOIN {$wpdb->postmeta} pm ON c.attendee_id = pm.post_id + WHERE pm.meta_key = '_tribe_tickets_email' AND pm.meta_value LIKE %s + ) + )"; + $where_values[] = $search_term; + $where_values[] = $search_term; + } + + // Build WHERE clause + $where_clause = !empty($where) ? "WHERE " . implode(" AND ", $where) : ""; + + // Build final query + $query = "SELECT COUNT(*) FROM {$wpdb->prefix}hvac_certificates $where_clause"; + + // Prepare the query if we have where values + if (!empty($where_values)) { + $query = $wpdb->prepare($query, $where_values); + } + + $count = $wpdb->get_var($query); + + return intval($count); + + } catch (Exception $e) { + return 0; + } + } + + /** + * Get certificate statistics for a specific user. + * + * @param int $user_id The user ID. + * + * @return array Certificate statistics. + */ + public function get_user_certificate_stats($user_id) { + global $wpdb; + + // Default empty stats + $empty_stats = array( + 'total' => 0, + 'active' => 0, + 'revoked' => 0, + 'emailed' => 0 + ); + + try { + // Check if table exists before querying + $table_name = $wpdb->prefix . 'hvac_certificates'; + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name; + + if (!$table_exists) { + return $empty_stats; + } + + // Use direct database query to get user's event IDs (bypassing TEC interference) + $event_ids = $wpdb->get_col($wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} + WHERE post_type = %s + AND post_author = %d + AND post_status = 'publish'", + 'tribe_events', + $user_id + )); + + if (empty($event_ids)) { + return $empty_stats; + } + + // Create string of event IDs for query + $event_ids_string = implode(',', array_map('intval', $event_ids)); + + if (empty($event_ids_string)) { + return $empty_stats; + } + + $query = "SELECT + COUNT(*) as total, + SUM(CASE WHEN revoked = 0 THEN 1 ELSE 0 END) as active, + SUM(CASE WHEN revoked = 1 THEN 1 ELSE 0 END) as revoked, + SUM(CASE WHEN email_sent = 1 THEN 1 ELSE 0 END) as emailed + FROM {$wpdb->prefix}hvac_certificates + WHERE event_id IN ($event_ids_string)"; + + $result = $wpdb->get_row($query); + + if ($wpdb->last_error) { + return $empty_stats; + } + + if (is_null($result)) { + return $empty_stats; + } + + $stats = array( + 'total' => intval($result->total), + 'active' => intval($result->active), + 'revoked' => intval($result->revoked), + 'emailed' => intval($result->emailed) + ); + + return $stats; + } catch (Exception $e) { + return $empty_stats; + } + } + + /** + * Get certificate file path. + * + * @param int $certificate_id The certificate ID. + * + * @return string|false The file path if found, false otherwise. + */ + public function get_certificate_file_path($certificate_id) { + $certificate = $this->get_certificate($certificate_id); + + if (!$certificate) { + return false; + } + + // Get uploads directory + $upload_dir = wp_upload_dir(); + $base_dir = $upload_dir['basedir']; + + // Construct full path + $full_path = $base_dir . '/' . $certificate->file_path; + + if (file_exists($full_path)) { + return $full_path; + } + + return false; + } + + /** + * Get certificate file URL. + * + * @param int $certificate_id The certificate ID. + * + * @return string|false The file URL if found, false otherwise. + */ + public function get_certificate_url($certificate_id) { + // Create a secure URL with nonce for downloading + $url = add_query_arg( + array( + 'action' => 'hvac_download_certificate', + 'certificate_id' => $certificate_id, + 'nonce' => wp_create_nonce('download_certificate_' . $certificate_id) + ), + admin_url('admin-ajax.php') + ); + + return $url; + } + + /** + * Check if an attendee already has a certificate for an event. + * + * @param int $event_id The event ID. + * @param int $attendee_id The attendee ID. + * + * @return bool True if a certificate exists, false otherwise. + */ + public function certificate_exists($event_id, $attendee_id) { + $certificate = $this->get_certificate_by_attendee($event_id, $attendee_id); + return !empty($certificate); + } + + /** + * Delete a certificate record and its file. + * + * @param int $certificate_id The certificate ID. + * + * @return bool True if successful, false otherwise. + */ + public function delete_certificate($certificate_id) { + global $wpdb; + + // Get certificate to get file path + $certificate = $this->get_certificate($certificate_id); + + if (!$certificate) { + return false; + } + + // Delete file if it exists + if (!empty($certificate->file_path)) { + $upload_dir = wp_upload_dir(); + $full_path = $upload_dir['basedir'] . '/' . $certificate->file_path; + + if (file_exists($full_path)) { + unlink($full_path); + } + } + + // Delete from database + $result = $wpdb->delete( + $wpdb->prefix . 'hvac_certificates', + array('certificate_id' => $certificate_id), + array('%d') + ); + + return $result !== false; + } +} \ No newline at end of file diff --git a/includes/certificates/class-certificate-security.php b/includes/certificates/class-certificate-security.php new file mode 100644 index 00000000..67f9abe2 --- /dev/null +++ b/includes/certificates/class-certificate-security.php @@ -0,0 +1,322 @@ +validate_download_token($certificate_token); + + if (!$certificate_data) { + wp_die(__('Invalid or expired certificate download link.', 'hvac-community-events')); + } + + // Get file path + $file_path = $this->get_certificate_file_path($certificate_data); + + if (!$file_path || !file_exists($file_path)) { + wp_die(__('Certificate file not found.', 'hvac-community-events')); + } + + // Serve the file + $this->serve_certificate_file($file_path, $certificate_data); + exit; + } + + /** + * Parse certificate requests directly without relying on rewrite rules. + * This is a fallback method that works even if rewrite rules fail. + */ + public function parse_certificate_request($wp) { + // Only process if we haven't already handled via template_redirect + if (did_action('template_redirect')) { + return; + } + + $request_uri = $_SERVER['REQUEST_URI']; + + // Only match exact certificate download URLs - be very specific + if (preg_match('#^/hvac-certificate/([a-zA-Z0-9]{32})/?$#', $request_uri, $matches)) { + $certificate_token = $matches[1]; + + // Validate the token exists (don't delete it yet - let the normal handler do that) + $certificate_data = get_transient('hvac_certificate_token_' . $certificate_token); + + if (!$certificate_data) { + // Return 404 instead of wp_die to avoid interfering with other pages + status_header(404); + return; + } + + // If we have valid certificate data, let the normal template_redirect handler take over + // Set the query var so the normal handler can pick it up + set_query_var('certificate_token', $certificate_token); + return; + } + } + + /** + * Validate a certificate download token. + * + * @param string $token The token to validate. + * + * @return array|false Certificate data if valid, false otherwise. + */ + protected function validate_download_token($token) { + // Check if token exists in transients + $certificate_data = get_transient('hvac_certificate_token_' . $token); + + if (!$certificate_data) { + return false; + } + + // Delete the transient to prevent reuse + delete_transient('hvac_certificate_token_' . $token); + + return $certificate_data; + } + + /** + * Get the full file path for a certificate. + * + * @param array $certificate_data Certificate data. + * + * @return string|false Full file path or false if not found. + */ + protected function get_certificate_file_path($certificate_data) { + if (empty($certificate_data['file_path'])) { + return false; + } + + $upload_dir = wp_upload_dir(); + $file_path = $upload_dir['basedir'] . '/' . $certificate_data['file_path']; + + if (file_exists($file_path)) { + return $file_path; + } + + return false; + } + + /** + * Serve a certificate file for download. + * + * @param string $file_path Full path to certificate file. + * @param array $certificate_data Certificate data. + */ + protected function serve_certificate_file($file_path, $certificate_data) { + // Get file information + $file_name = basename($file_path); + $file_size = filesize($file_path); + $file_ext = pathinfo($file_path, PATHINFO_EXTENSION); + + // Set download filename + $event_name = sanitize_title($certificate_data['event_name'] ?? 'event'); + $attendee_name = sanitize_title($certificate_data['attendee_name'] ?? 'attendee'); + $download_filename = "certificate-{$event_name}-{$attendee_name}.{$file_ext}"; + + // Send headers + nocache_headers(); + header('Content-Type: application/pdf'); + header('Content-Disposition: attachment; filename="' . $download_filename . '"'); + header('Content-Transfer-Encoding: binary'); + header('Content-Length: ' . $file_size); + + // Disable output buffering + if (ob_get_level()) { + ob_end_clean(); + } + + // Output the file + readfile($file_path); + } + + /** + * Generate a secure download token for a certificate. + * + * @param int $certificate_id The certificate ID. + * @param array $certificate_data Additional certificate data. + * @param int $expiry Token expiry time in seconds (default 1 hour). + * + * @return string|false The download URL or false on failure. + */ + public function generate_download_token($certificate_id, $certificate_data, $expiry = 3600) { + if (!$certificate_id || empty($certificate_data['file_path'])) { + return false; + } + + // Generate a unique token + $token = wp_generate_password(32, false); + + // Store in transient + set_transient('hvac_certificate_token_' . $token, $certificate_data, $expiry); + + // Generate URL + return home_url('hvac-certificate/' . $token); + } + + /** + * Create a secure storage directory for certificates. + * + * @param string $dir_path The directory path to secure. + * + * @return bool True if successful, false otherwise. + */ + public function create_secure_directory($dir_path) { + // Check if directory exists + if (!file_exists($dir_path)) { + // Create directory + if (!wp_mkdir_p($dir_path)) { + return false; + } + } + + // Create/update .htaccess file + $htaccess_content = "# Prevent direct access to files\n"; + $htaccess_content .= "\n"; + $htaccess_content .= " Order Allow,Deny\n"; + $htaccess_content .= " Deny from all\n"; + $htaccess_content .= "\n"; + $htaccess_content .= "# Prevent directory listing\n"; + $htaccess_content .= "Options -Indexes\n"; + + $htaccess_file = $dir_path . '/.htaccess'; + + if (!@file_put_contents($htaccess_file, $htaccess_content)) { + return false; + } + + // Create empty index.php + $index_content = "init_secure_download(); + + // Flush the rules + flush_rewrite_rules(); + + // Log the action + if (class_exists('HVAC_Logger')) { + HVAC_Logger::info('Rewrite rules flushed manually via admin parameter', 'Certificate Security'); + } + + // Redirect to remove the parameter + wp_redirect(remove_query_arg('hvac_flush_rewrite_rules')); + exit; + } + } +} \ No newline at end of file diff --git a/includes/certificates/class-certificate-settings.php b/includes/certificates/class-certificate-settings.php new file mode 100644 index 00000000..661e4431 --- /dev/null +++ b/includes/certificates/class-certificate-settings.php @@ -0,0 +1,200 @@ +maybe_initialize_settings(); + } + + /** + * Initialize default certificate settings if they don't exist. + */ + public function maybe_initialize_settings() { + // Certificate counter for unique numbers + if (false === get_option('hvac_certificate_counter')) { + add_option('hvac_certificate_counter', 0); + } + + // Certificate number prefix + if (false === get_option('hvac_certificate_prefix')) { + add_option('hvac_certificate_prefix', 'HVAC-'); + } + + // Certificate storage path (relative to wp-content/uploads/) + if (false === get_option('hvac_certificate_storage_path')) { + add_option('hvac_certificate_storage_path', 'hvac-certificates'); + } + + // Certificate paper size + if (false === get_option('hvac_certificate_paper_size')) { + add_option('hvac_certificate_paper_size', 'LETTER'); // LETTER, A4, etc. + } + + // Certificate orientation + if (false === get_option('hvac_certificate_orientation')) { + add_option('hvac_certificate_orientation', 'L'); // L for landscape, P for portrait + } + + // Certificate background color + if (false === get_option('hvac_certificate_bg_color')) { + add_option('hvac_certificate_bg_color', '#ffffff'); + } + + // Certificate border color + if (false === get_option('hvac_certificate_border_color')) { + add_option('hvac_certificate_border_color', '#0074be'); + } + + // Certificate title text + if (false === get_option('hvac_certificate_title_text')) { + add_option('hvac_certificate_title_text', 'CERTIFICATE OF COMPLETION'); + } + + // Certificate title color + if (false === get_option('hvac_certificate_title_color')) { + add_option('hvac_certificate_title_color', '#0074be'); + } + + // Certificate body text color + if (false === get_option('hvac_certificate_text_color')) { + add_option('hvac_certificate_text_color', '#333333'); + } + + // Certificate completion text + if (false === get_option('hvac_certificate_completion_text')) { + add_option('hvac_certificate_completion_text', 'This certificate is awarded to {attendee_name} for successfully completing {event_name} on {event_date}.'); + } + } + + /** + * Get all certificate settings. + * + * @return array All certificate settings. + */ + public function get_all_settings() { + return array( + 'counter' => get_option('hvac_certificate_counter', 0), + 'prefix' => get_option('hvac_certificate_prefix', 'HVAC-'), + 'storage_path' => get_option('hvac_certificate_storage_path', 'hvac-certificates'), + 'paper_size' => get_option('hvac_certificate_paper_size', 'LETTER'), + 'orientation' => get_option('hvac_certificate_orientation', 'L'), + 'bg_color' => get_option('hvac_certificate_bg_color', '#ffffff'), + 'border_color' => get_option('hvac_certificate_border_color', '#0074be'), + 'title_text' => get_option('hvac_certificate_title_text', 'CERTIFICATE OF COMPLETION'), + 'title_color' => get_option('hvac_certificate_title_color', '#0074be'), + 'text_color' => get_option('hvac_certificate_text_color', '#333333'), + 'completion_text' => get_option('hvac_certificate_completion_text', 'This certificate is awarded to {attendee_name} for successfully completing {event_name} on {event_date}.') + ); + } + + /** + * Update a certificate setting. + * + * @param string $setting The setting key. + * @param mixed $value The setting value. + * + * @return bool True if successful, false otherwise. + */ + public function update_setting($setting, $value) { + $option_name = 'hvac_certificate_' . $setting; + + return update_option($option_name, $value); + } + + /** + * Get a certificate setting. + * + * @param string $setting The setting key. + * @param mixed $default Optional default value. + * + * @return mixed The setting value or default. + */ + public function get_setting($setting, $default = '') { + $option_name = 'hvac_certificate_' . $setting; + + return get_option($option_name, $default); + } + + /** + * Get available certificate placeholders. + * + * @return array Placeholders and their descriptions. + */ + public function get_placeholders() { + return array( + '{attendee_name}' => 'The full name of the attendee', + '{event_name}' => 'The name of the event', + '{event_date}' => 'The date when the event occurred', + '{organization_name}' => 'The name of the training organization', + '{instructor_name}' => 'The name of the instructor', + '{venue_name}' => 'The name of the venue', + '{certificate_number}' => 'The unique certificate number', + '{issue_date}' => 'The date when the certificate was issued' + ); + } + + /** + * Replace placeholders in text with actual values. + * + * @param string $text The text with placeholders. + * @param array $data The data to replace placeholders with. + * + * @return string The text with placeholders replaced. + */ + public function replace_placeholders($text, $data) { + $placeholders = array_keys($this->get_placeholders()); + $replacements = array(); + + foreach ($placeholders as $placeholder) { + $key = str_replace(array('{', '}'), '', $placeholder); + $replacements[] = isset($data[$key]) ? $data[$key] : ''; + } + + return str_replace($placeholders, $replacements, $text); + } +} \ No newline at end of file diff --git a/includes/certificates/class-certificate-template.php b/includes/certificates/class-certificate-template.php new file mode 100644 index 00000000..ce1e8062 --- /dev/null +++ b/includes/certificates/class-certificate-template.php @@ -0,0 +1,437 @@ +settings = HVAC_Certificate_Settings::instance(); + + // Initialize hooks + $this->init_hooks(); + } + + /** + * Initialize hooks. + */ + protected function init_hooks() { + // Add AJAX handlers for template preview + add_action('wp_ajax_hvac_preview_certificate', array($this, 'ajax_preview_certificate')); + + // Add action to register custom upload folder + add_filter('upload_dir', array($this, 'certificate_upload_dir')); + } + + /** + * Modify the upload directory for certificate files. + * + * @param array $dirs Upload directory information. + * + * @return array Modified upload directory. + */ + public function certificate_upload_dir($dirs) { + // Only modify for certificate uploads + if (isset($_POST['certificate_upload']) && $_POST['certificate_upload'] === 'true') { + $certificate_dir = $this->settings->get_setting('storage_path', 'hvac-certificates'); + + $dirs['subdir'] = '/' . $certificate_dir; + $dirs['path'] = $dirs['basedir'] . $dirs['subdir']; + $dirs['url'] = $dirs['baseurl'] . $dirs['subdir']; + } + + return $dirs; + } + + /** + * Get available certificate templates. + * + * @return array List of certificate templates. + */ + public function get_templates() { + $templates = array( + 'default' => array( + 'name' => __('Default', 'hvac-community-events'), + 'description' => __('Standard certificate template with blue accents', 'hvac-community-events'), + 'background' => HVAC_PLUGIN_URL . 'assets/images/certificate-background.jpg', + 'thumbnail' => HVAC_PLUGIN_URL . 'assets/images/certificate-background-thumb.jpg', + ), + ); + + // Allow filtering of templates + return apply_filters('hvac_certificate_templates', $templates); + } + + /** + * Get the current certificate template. + * + * @return array The current template settings. + */ + public function get_current_template() { + $template_id = $this->settings->get_setting('template', 'default'); + $templates = $this->get_templates(); + + if (isset($templates[$template_id])) { + return $templates[$template_id]; + } + + // Fallback to default + return $templates['default']; + } + + /** + * Get the path to the certificate background image. + * + * @return string|false The path to the background image or false if not found. + */ + public function get_background_path() { + // Check for custom uploaded background first + $custom_bg = $this->settings->get_setting('custom_background', ''); + + if (!empty($custom_bg)) { + $upload_dir = wp_upload_dir(); + $file_path = $upload_dir['basedir'] . '/' . $custom_bg; + + if (file_exists($file_path)) { + return $file_path; + } + } + + // Fallback to default template background + $default_bg = HVAC_PLUGIN_DIR . 'assets/images/certificate-background.jpg'; + + if (file_exists($default_bg)) { + return $default_bg; + } + + return false; + } + + /** + * Get the path to the certificate logo image. + * + * @return string|false The path to the logo image or false if not found. + */ + public function get_logo_path() { + // Check for custom uploaded logo first + $custom_logo = $this->settings->get_setting('custom_logo', ''); + + if (!empty($custom_logo)) { + $upload_dir = wp_upload_dir(); + $file_path = $upload_dir['basedir'] . '/' . $custom_logo; + + if (file_exists($file_path)) { + return $file_path; + } + } + + // Fallback to default logo + $default_logo = HVAC_PLUGIN_DIR . 'assets/images/certificate-logo.png'; + + if (file_exists($default_logo)) { + return $default_logo; + } + + return false; + } + + /** + * Generate a preview certificate for the settings page. + * + * @return string Path to the preview certificate file. + */ + public function generate_preview() { + // Load TCPDF if not already included + if (!class_exists('TCPDF')) { + require_once HVAC_PLUGIN_DIR . 'vendor/tecnickcom/tcpdf/tcpdf.php'; + } + + // Create PDF document + $pdf = new TCPDF( + $this->settings->get_setting('orientation', 'L'), + 'mm', + $this->settings->get_setting('paper_size', 'LETTER'), + true, + 'UTF-8', + false + ); + + // Set document information + $pdf->SetCreator('HVAC Community Events'); + $pdf->SetAuthor('Upskill HVAC'); + $pdf->SetTitle('Certificate Preview'); + + // Set margins + $pdf->SetMargins(15, 15, 15); + + // Remove default header/footer + $pdf->setPrintHeader(false); + $pdf->setPrintFooter(false); + + // Set auto page breaks + $pdf->SetAutoPageBreak(false, 0); + + // Add a page + $pdf->AddPage(); + + // Get background image if available + $bg_path = $this->get_background_path(); + + if ($bg_path) { + // Add background + $pdf->Image($bg_path, 0, 0, $pdf->getPageWidth(), $pdf->getPageHeight(), '', '', '', false, 300); + } else { + // Create a simple background with border + $this->render_default_background($pdf); + } + + // Add logo if available + $logo_path = $this->get_logo_path(); + + if ($logo_path) { + $pdf->Image($logo_path, 15, 15, 40, 0, '', '', '', false, 300); + } + + // Render sample content + $this->render_preview_content($pdf); + + // Create upload directory if it doesn't exist + $upload_dir = wp_upload_dir(); + $preview_dir = $upload_dir['basedir'] . '/hvac-certificate-previews'; + + if (!file_exists($preview_dir)) { + wp_mkdir_p($preview_dir); + } + + // Create an htaccess file to prevent direct access + $htaccess_file = $preview_dir . '/.htaccess'; + if (!file_exists($htaccess_file)) { + $htaccess_content = "# Prevent direct access to files\n"; + $htaccess_content .= "\n"; + $htaccess_content .= " Order Allow,Deny\n"; + $htaccess_content .= " Deny from all\n"; + $htaccess_content .= ""; + + @file_put_contents($htaccess_file, $htaccess_content); + } + + // Define preview file path + $preview_file = 'certificate-preview-' . time() . '.pdf'; + $preview_path = $preview_dir . '/' . $preview_file; + + // Save PDF + $pdf->Output($preview_path, 'F'); + + // Return relative path to preview file + return 'hvac-certificate-previews/' . $preview_file; + } + + /** + * Render the default background for a certificate. + * + * @param TCPDF $pdf The PDF object. + */ + protected function render_default_background($pdf) { + // Get background color + $bg_color = $this->hex_to_rgb($this->settings->get_setting('bg_color', '#ffffff')); + + // Fill background + $pdf->SetFillColor($bg_color[0], $bg_color[1], $bg_color[2]); + $pdf->Rect(0, 0, $pdf->getPageWidth(), $pdf->getPageHeight(), 'F'); + + // Add border + $border_color = $this->hex_to_rgb($this->settings->get_setting('border_color', '#0074be')); + $pdf->SetDrawColor($border_color[0], $border_color[1], $border_color[2]); + $pdf->SetLineWidth(1.5); + $pdf->Rect(5, 5, $pdf->getPageWidth() - 10, $pdf->getPageHeight() - 10, 'D'); + + // Add inner border + $pdf->SetDrawColor(200, 200, 200); // Light gray + $pdf->SetLineWidth(0.5); + $pdf->Rect(10, 10, $pdf->getPageWidth() - 20, $pdf->getPageHeight() - 20, 'D'); + } + + /** + * Render content for the preview certificate. + * + * @param TCPDF $pdf The PDF object. + */ + protected function render_preview_content($pdf) { + // Get title color + $title_color = $this->hex_to_rgb($this->settings->get_setting('title_color', '#0074be')); + + // Get text color + $text_color = $this->hex_to_rgb($this->settings->get_setting('text_color', '#333333')); + + // Certificate title + $pdf->SetFont('helvetica', 'B', 30); + $pdf->SetTextColor($title_color[0], $title_color[1], $title_color[2]); + $pdf->SetY(30); + $pdf->Cell(0, 20, $this->settings->get_setting('title_text', 'CERTIFICATE OF COMPLETION'), 0, 1, 'C'); + + // Description text + $pdf->SetFont('helvetica', '', 12); + $pdf->SetTextColor($text_color[0], $text_color[1], $text_color[2]); + $pdf->SetY(55); + $pdf->Cell(0, 10, 'This certificate is awarded to', 0, 1, 'C'); + + // Attendee name + $pdf->SetFont('helvetica', 'B', 24); + $pdf->SetTextColor(0, 0, 0); // Black + $pdf->Cell(0, 15, 'John Smith', 0, 1, 'C'); + + // Course completion text + $pdf->SetFont('helvetica', '', 12); + $pdf->SetTextColor($text_color[0], $text_color[1], $text_color[2]); + $pdf->Cell(0, 10, 'for successfully completing', 0, 1, 'C'); + + // Event name + $pdf->SetFont('helvetica', 'B', 18); + $pdf->SetTextColor($title_color[0], $title_color[1], $title_color[2]); + $pdf->Cell(0, 15, 'Advanced HVAC Troubleshooting Workshop', 0, 1, 'C'); + + // Event date + $pdf->SetFont('helvetica', '', 12); + $pdf->SetTextColor($text_color[0], $text_color[1], $text_color[2]); + $pdf->Cell(0, 10, 'on June 15, 2025', 0, 1, 'C'); + + // Draw a line + $pdf->SetDrawColor($title_color[0], $title_color[1], $title_color[2]); + $pdf->SetLineWidth(0.5); + $pdf->Line(70, 150, 190, 150); + + // Instructor name + $pdf->SetY(155); + $pdf->SetFont('helvetica', 'B', 12); + $pdf->SetTextColor(0, 0, 0); // Black + $pdf->Cell(0, 10, 'Sarah Johnson', 0, 1, 'C'); + + $pdf->SetFont('helvetica', '', 10); + $pdf->SetTextColor($text_color[0], $text_color[1], $text_color[2]); + $pdf->Cell(0, 10, 'Instructor', 0, 1, 'C'); + + // Add organization name + $pdf->SetY(175); + $pdf->SetFont('helvetica', 'B', 10); + $pdf->SetTextColor(0, 0, 0); // Black + $pdf->Cell(0, 10, 'Upskill HVAC', 0, 1, 'C'); + + // Add venue info + $pdf->SetFont('helvetica', '', 10); + $pdf->SetTextColor($text_color[0], $text_color[1], $text_color[2]); + $pdf->Cell(0, 10, 'Technical Training Center, Boston', 0, 1, 'C'); + + // Add certificate details at the bottom + $pdf->SetFont('helvetica', '', 8); + $pdf->SetTextColor(128, 128, 128); // Light gray + $pdf->SetY(195); + $pdf->Cell(0, 10, 'Certificate #: HVAC-12345 | Issue Date: June 16, 2025', 0, 1, 'C'); + } + + /** + * AJAX handler for certificate preview generation. + */ + public function ajax_preview_certificate() { + // Verify nonce + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'hvac_certificate_preview')) { + wp_send_json_error(array('message' => 'Security check failed')); + } + + // Check user capabilities + if (!current_user_can('manage_options')) { + wp_send_json_error(array('message' => 'Insufficient permissions')); + } + + // Update settings first + if (isset($_POST['settings']) && is_array($_POST['settings'])) { + foreach ($_POST['settings'] as $key => $value) { + $this->settings->update_setting($key, sanitize_text_field($value)); + } + } + + // Generate preview + $preview_path = $this->generate_preview(); + + // Get full URL to preview + $upload_dir = wp_upload_dir(); + $preview_url = $upload_dir['baseurl'] . '/' . $preview_path; + + wp_send_json_success(array( + 'preview_url' => $preview_url, + 'message' => 'Preview generated successfully' + )); + } + + /** + * Convert hexadecimal color to RGB. + * + * @param string $hex The hexadecimal color code. + * + * @return array RGB values. + */ + protected function hex_to_rgb($hex) { + // Remove # if present + $hex = ltrim($hex, '#'); + + if (strlen($hex) == 3) { + $r = hexdec(substr($hex, 0, 1) . substr($hex, 0, 1)); + $g = hexdec(substr($hex, 1, 1) . substr($hex, 1, 1)); + $b = hexdec(substr($hex, 2, 1) . substr($hex, 2, 1)); + } else { + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); + } + + return array($r, $g, $b); + } +} \ No newline at end of file diff --git a/includes/certificates/class-certificate-url-handler.php b/includes/certificates/class-certificate-url-handler.php new file mode 100644 index 00000000..13f3b9d2 --- /dev/null +++ b/includes/certificates/class-certificate-url-handler.php @@ -0,0 +1,213 @@ +handle_certificate_download($token); + exit; // Stop WordPress from processing further + } + } + + /** + * Handle certificate download + */ + protected function handle_certificate_download($token) { + // Validate the token + $certificate_data = $this->validate_download_token($token); + + if (!$certificate_data) { + if (class_exists('HVAC_Logger')) { + HVAC_Logger::error("Invalid or expired certificate token: $token", 'Certificates'); + } + wp_die(__('Invalid or expired certificate download link.', 'hvac-community-events'), 'Certificate Error', array('response' => 404)); + } + + // Get file path + $file_path = $this->get_certificate_file_path($certificate_data); + + if (!$file_path || !file_exists($file_path)) { + if (class_exists('HVAC_Logger')) { + HVAC_Logger::error("Certificate file not found for token: $token", 'Certificates'); + } + wp_die(__('Certificate file not found.', 'hvac-community-events'), 'Certificate Error', array('response' => 404)); + } + + // Log successful access + if (class_exists('HVAC_Logger')) { + HVAC_Logger::info("Serving certificate file: $file_path", 'Certificates'); + } + + // Serve the file + $this->serve_certificate_file($file_path, $certificate_data); + } + + /** + * Validate a certificate download token. + */ + protected function validate_download_token($token) { + // Check if token exists in transients + $certificate_data = get_transient('hvac_certificate_token_' . $token); + + if (!$certificate_data) { + return false; + } + + // Delete the transient to prevent reuse + delete_transient('hvac_certificate_token_' . $token); + + return $certificate_data; + } + + /** + * Get the full file path for a certificate. + */ + protected function get_certificate_file_path($certificate_data) { + if (empty($certificate_data['file_path'])) { + return false; + } + + $upload_dir = wp_upload_dir(); + $file_path = $upload_dir['basedir'] . '/' . $certificate_data['file_path']; + + if (file_exists($file_path)) { + return $file_path; + } + + return false; + } + + /** + * Serve a certificate file for download. + */ + protected function serve_certificate_file($file_path, $certificate_data) { + // Get file information + $file_name = basename($file_path); + $file_size = filesize($file_path); + $file_ext = pathinfo($file_path, PATHINFO_EXTENSION); + + // Set download filename + $event_name = sanitize_title($certificate_data['event_name'] ?? 'event'); + $attendee_name = sanitize_title($certificate_data['attendee_name'] ?? 'attendee'); + $download_filename = "certificate-{$event_name}-{$attendee_name}.{$file_ext}"; + + // Send headers + nocache_headers(); + header('Content-Type: application/pdf'); + header('Content-Disposition: attachment; filename="' . $download_filename . '"'); + header('Content-Transfer-Encoding: binary'); + header('Content-Length: ' . $file_size); + + // Disable output buffering + if (ob_get_level()) { + ob_end_clean(); + } + + // Output the file + readfile($file_path); + } + + /** + * Parse request fallback method + */ + public function parse_certificate_request($wp) { + // Get the request URI + $request_uri = $_SERVER['REQUEST_URI']; + $parsed_url = parse_url($request_uri); + $path = $parsed_url['path'] ?? ''; + + // Remove any trailing slash for consistency + $path = rtrim($path, '/'); + + // Check if this is a certificate URL + if (preg_match('#^/hvac-certificate/([a-zA-Z0-9]{32})$#', $path, $matches)) { + $token = $matches[1]; + + // Log the request + if (class_exists('HVAC_Logger')) { + HVAC_Logger::info("Certificate URL detected via parse_request - Token: $token", 'Certificates'); + } + + // Handle the certificate download + $this->handle_certificate_download($token); + exit; // Stop WordPress from processing further + } + } +} \ No newline at end of file diff --git a/includes/certificates/test-rewrite-rules.php b/includes/certificates/test-rewrite-rules.php new file mode 100644 index 00000000..8ae1637e --- /dev/null +++ b/includes/certificates/test-rewrite-rules.php @@ -0,0 +1,71 @@ +Certificate Rewrite Rules Test'; + echo '
';
+        
+        // Check if our rewrite rule exists
+        $rules = $wp_rewrite->wp_rewrite_rules();
+        $found = false;
+        
+        echo "Looking for certificate rewrite rule...\n\n";
+        
+        foreach ($rules as $pattern => $redirect) {
+            if (strpos($pattern, 'hvac-certificate') !== false) {
+                echo "✅ FOUND: $pattern => $redirect\n";
+                $found = true;
+            }
+        }
+        
+        if (!$found) {
+            echo "❌ Certificate rewrite rule NOT FOUND!\n\n";
+            echo "Attempting to add rule and flush...\n";
+            
+            // Try to add the rule
+            add_rewrite_rule(
+                'hvac-certificate/([^/]+)/?$',
+                'index.php?certificate_token=$matches[1]',
+                'top'
+            );
+            
+            // Flush rules
+            flush_rewrite_rules();
+            
+            echo "Rules flushed. Refresh to check again.\n";
+        }
+        
+        // Check query vars
+        echo "\n\nRegistered Query Vars:\n";
+        global $wp;
+        if (in_array('certificate_token', $wp->public_query_vars)) {
+            echo "✅ certificate_token is registered\n";
+        } else {
+            echo "❌ certificate_token is NOT registered\n";
+        }
+        
+        // Show all rewrite rules (limited)
+        echo "\n\nFirst 20 Rewrite Rules:\n";
+        $count = 0;
+        foreach ($rules as $pattern => $redirect) {
+            echo "$pattern => $redirect\n";
+            if (++$count >= 20) break;
+        }
+        
+        echo '
'; + + echo '

Return to Admin

'; + + die(); + } +}); \ No newline at end of file diff --git a/includes/communication/class-communication-installer.php b/includes/communication/class-communication-installer.php new file mode 100644 index 00000000..46200d60 --- /dev/null +++ b/includes/communication/class-communication-installer.php @@ -0,0 +1,383 @@ +get_charset_collate(); + + self::create_schedules_table( $charset_collate ); + self::create_logs_table( $charset_collate ); + self::create_tracking_table( $charset_collate ); + + // Update version option + update_option( 'hvac_communication_db_version', self::DB_VERSION ); + + if ( class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::info( 'Communication system database tables installed', 'Communication Installer' ); + } + } + + /** + * Create communication schedules table + * + * @param string $charset_collate Database charset and collation + */ + private static function create_schedules_table( $charset_collate ) { + global $wpdb; + + $table_name = $wpdb->prefix . 'hvac_communication_schedules'; + + $sql = "CREATE TABLE {$table_name} ( + schedule_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + trainer_id BIGINT(20) UNSIGNED NOT NULL, + event_id BIGINT(20) UNSIGNED DEFAULT NULL, + template_id BIGINT(20) UNSIGNED NOT NULL, + schedule_name VARCHAR(255) NOT NULL DEFAULT '', + schedule_type VARCHAR(50) NOT NULL DEFAULT 'time_based', + trigger_type VARCHAR(50) NOT NULL, + trigger_value INT(11) NOT NULL DEFAULT 0, + trigger_unit VARCHAR(20) NOT NULL DEFAULT 'days', + status VARCHAR(20) NOT NULL DEFAULT 'active', + target_audience VARCHAR(50) NOT NULL DEFAULT 'all_attendees', + custom_recipient_list TEXT DEFAULT NULL, + conditions TEXT DEFAULT NULL, + next_run DATETIME DEFAULT NULL, + last_run DATETIME DEFAULT NULL, + run_count INT(11) NOT NULL DEFAULT 0, + is_recurring TINYINT(1) NOT NULL DEFAULT 0, + recurring_interval INT(11) DEFAULT NULL, + recurring_unit VARCHAR(20) DEFAULT NULL, + max_runs INT(11) DEFAULT NULL, + created_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + modified_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (schedule_id), + KEY trainer_id (trainer_id), + KEY event_id (event_id), + KEY template_id (template_id), + KEY status (status), + KEY trigger_type (trigger_type), + KEY next_run (next_run), + KEY created_date (created_date) + ) {$charset_collate};"; + + dbDelta( $sql ); + } + + /** + * Create communication logs table + * + * @param string $charset_collate Database charset and collation + */ + private static function create_logs_table( $charset_collate ) { + global $wpdb; + + $table_name = $wpdb->prefix . 'hvac_communication_logs'; + + $sql = "CREATE TABLE {$table_name} ( + log_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + schedule_id BIGINT(20) UNSIGNED NOT NULL, + recipient_email VARCHAR(255) DEFAULT NULL, + status VARCHAR(20) NOT NULL, + sent_date DATETIME NOT NULL, + recipient_count INT(11) NOT NULL DEFAULT 0, + success_count INT(11) NOT NULL DEFAULT 0, + error_count INT(11) NOT NULL DEFAULT 0, + execution_time DECIMAL(8,4) NOT NULL DEFAULT 0.0000, + details TEXT DEFAULT NULL, + PRIMARY KEY (log_id), + KEY schedule_id (schedule_id), + KEY status (status), + KEY sent_date (sent_date), + KEY recipient_email (recipient_email) + ) {$charset_collate};"; + + dbDelta( $sql ); + } + + /** + * Create event communication tracking table + * + * @param string $charset_collate Database charset and collation + */ + private static function create_tracking_table( $charset_collate ) { + global $wpdb; + + $table_name = $wpdb->prefix . 'hvac_event_communication_tracking'; + + $sql = "CREATE TABLE {$table_name} ( + tracking_id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + event_id BIGINT(20) UNSIGNED NOT NULL, + attendee_id BIGINT(20) UNSIGNED NOT NULL, + schedule_id BIGINT(20) UNSIGNED NOT NULL, + email VARCHAR(255) NOT NULL, + sent_date DATETIME NOT NULL, + delivery_status VARCHAR(20) NOT NULL DEFAULT 'sent', + opened TINYINT(1) NOT NULL DEFAULT 0, + opened_date DATETIME DEFAULT NULL, + clicked TINYINT(1) NOT NULL DEFAULT 0, + clicked_date DATETIME DEFAULT NULL, + bounced TINYINT(1) NOT NULL DEFAULT 0, + bounce_reason TEXT DEFAULT NULL, + PRIMARY KEY (tracking_id), + UNIQUE KEY event_attendee_schedule (event_id, attendee_id, schedule_id), + KEY event_id (event_id), + KEY attendee_id (attendee_id), + KEY schedule_id (schedule_id), + KEY email (email), + KEY delivery_status (delivery_status), + KEY sent_date (sent_date) + ) {$charset_collate};"; + + dbDelta( $sql ); + } + + /** + * Check if tables need to be updated + * + * @return bool True if update needed + */ + public static function needs_update() { + $installed_version = get_option( 'hvac_communication_db_version', '0' ); + return version_compare( $installed_version, self::DB_VERSION, '<' ); + } + + /** + * Update database tables if needed + */ + public static function maybe_update() { + if ( self::needs_update() ) { + self::install(); + } + } + + /** + * Check if all required tables exist + * + * @return bool True if all tables exist + */ + public static function tables_exist() { + global $wpdb; + + $required_tables = array( + $wpdb->prefix . 'hvac_communication_schedules', + $wpdb->prefix . 'hvac_communication_logs', + $wpdb->prefix . 'hvac_event_communication_tracking' + ); + + foreach ( $required_tables as $table ) { + if ( $wpdb->get_var( "SHOW TABLES LIKE '{$table}'" ) !== $table ) { + return false; + } + } + + return true; + } + + /** + * Drop all communication tables (for uninstall) + */ + public static function drop_tables() { + global $wpdb; + + $tables = array( + $wpdb->prefix . 'hvac_communication_schedules', + $wpdb->prefix . 'hvac_communication_logs', + $wpdb->prefix . 'hvac_event_communication_tracking' + ); + + foreach ( $tables as $table ) { + $wpdb->query( "DROP TABLE IF EXISTS {$table}" ); + } + + delete_option( 'hvac_communication_db_version' ); + + if ( class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::info( 'Communication system database tables dropped', 'Communication Installer' ); + } + } + + /** + * Get table status information + * + * @return array Table status information + */ + public static function get_table_status() { + global $wpdb; + + $tables = array( + 'schedules' => $wpdb->prefix . 'hvac_communication_schedules', + 'logs' => $wpdb->prefix . 'hvac_communication_logs', + 'tracking' => $wpdb->prefix . 'hvac_event_communication_tracking' + ); + + $status = array(); + + foreach ( $tables as $key => $table_name ) { + $exists = $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" ) === $table_name; + $count = 0; + + if ( $exists ) { + $count = $wpdb->get_var( "SELECT COUNT(*) FROM {$table_name}" ); + } + + $status[$key] = array( + 'table_name' => $table_name, + 'exists' => $exists, + 'record_count' => intval( $count ) + ); + } + + $status['db_version'] = get_option( 'hvac_communication_db_version', '0' ); + $status['current_version'] = self::DB_VERSION; + $status['needs_update'] = self::needs_update(); + + return $status; + } + + /** + * Repair corrupted tables + * + * @return array Repair results + */ + public static function repair_tables() { + global $wpdb; + + $tables = array( + $wpdb->prefix . 'hvac_communication_schedules', + $wpdb->prefix . 'hvac_communication_logs', + $wpdb->prefix . 'hvac_event_communication_tracking' + ); + + $results = array(); + + foreach ( $tables as $table ) { + $result = $wpdb->query( "REPAIR TABLE {$table}" ); + $results[$table] = $result !== false; + } + + return $results; + } + + /** + * Optimize database tables + * + * @return array Optimization results + */ + public static function optimize_tables() { + global $wpdb; + + $tables = array( + $wpdb->prefix . 'hvac_communication_schedules', + $wpdb->prefix . 'hvac_communication_logs', + $wpdb->prefix . 'hvac_event_communication_tracking' + ); + + $results = array(); + + foreach ( $tables as $table ) { + $result = $wpdb->query( "OPTIMIZE TABLE {$table}" ); + $results[$table] = $result !== false; + } + + return $results; + } + + /** + * Get database size information + * + * @return array Database size information + */ + public static function get_database_size() { + global $wpdb; + + $tables = array( + $wpdb->prefix . 'hvac_communication_schedules', + $wpdb->prefix . 'hvac_communication_logs', + $wpdb->prefix . 'hvac_event_communication_tracking' + ); + + $total_size = 0; + $table_sizes = array(); + + foreach ( $tables as $table ) { + $size_result = $wpdb->get_row( + $wpdb->prepare( + "SELECT + ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'size_mb' + FROM information_schema.TABLES + WHERE table_schema = %s + AND table_name = %s", + DB_NAME, + $table + ) + ); + + $size_mb = $size_result ? floatval( $size_result->size_mb ) : 0; + $table_sizes[$table] = $size_mb; + $total_size += $size_mb; + } + + return array( + 'total_size_mb' => round( $total_size, 2 ), + 'table_sizes' => $table_sizes + ); + } + + /** + * Create default communication schedules + */ + public static function create_default_schedules() { + // This would create some default schedule templates + // For now, we'll just log that defaults would be created + + if ( class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::info( 'Default communication schedules would be created here', 'Communication Installer' ); + } + } + + /** + * Migrate data from older versions + * + * @param string $from_version Version to migrate from + */ + public static function migrate_data( $from_version ) { + // Handle data migration between versions + // For now, this is a placeholder + + if ( class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::info( "Data migration from version {$from_version} to " . self::DB_VERSION, 'Communication Installer' ); + } + } +} \ No newline at end of file diff --git a/includes/communication/class-communication-logger.php b/includes/communication/class-communication-logger.php new file mode 100644 index 00000000..39de5c12 --- /dev/null +++ b/includes/communication/class-communication-logger.php @@ -0,0 +1,467 @@ +logs_table = $wpdb->prefix . 'hvac_communication_logs'; + } + + /** + * Log a schedule execution + * + * @param int $schedule_id Schedule ID + * @param string $status Execution status ('sent', 'failed', 'skipped') + * @param array $details Additional execution details + * @return int|false Log ID on success, false on failure + */ + public function log_schedule_execution( $schedule_id, $status, $details = array() ) { + global $wpdb; + + $log_data = array( + 'schedule_id' => intval( $schedule_id ), + 'status' => sanitize_text_field( $status ), + 'sent_date' => current_time( 'mysql' ), + 'recipient_count' => isset( $details['recipient_count'] ) ? intval( $details['recipient_count'] ) : 0, + 'success_count' => isset( $details['success_count'] ) ? intval( $details['success_count'] ) : 0, + 'error_count' => isset( $details['error_count'] ) ? intval( $details['error_count'] ) : 0, + 'execution_time' => isset( $details['execution_time'] ) ? floatval( $details['execution_time'] ) : 0, + 'details' => ! empty( $details ) ? wp_json_encode( $details ) : null + ); + + $formats = array( '%d', '%s', '%s', '%d', '%d', '%d', '%f', '%s' ); + + $result = $wpdb->insert( $this->logs_table, $log_data, $formats ); + + if ( $result === false ) { + if ( class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::error( 'Failed to log schedule execution: ' . $wpdb->last_error, 'Communication Logger' ); + } + return false; + } + + return $wpdb->insert_id; + } + + /** + * Log individual email delivery + * + * @param int $schedule_id Schedule ID + * @param string $recipient_email Recipient email address + * @param string $status Delivery status ('sent', 'failed', 'bounced') + * @param array $details Additional delivery details + * @return int|false Log ID on success, false on failure + */ + public function log_email_delivery( $schedule_id, $recipient_email, $status, $details = array() ) { + global $wpdb; + + $log_data = array( + 'schedule_id' => intval( $schedule_id ), + 'recipient_email' => sanitize_email( $recipient_email ), + 'status' => sanitize_text_field( $status ), + 'sent_date' => current_time( 'mysql' ), + 'details' => ! empty( $details ) ? wp_json_encode( $details ) : null + ); + + $formats = array( '%d', '%s', '%s', '%s', '%s' ); + + $result = $wpdb->insert( $this->logs_table, $log_data, $formats ); + + return $result !== false ? $wpdb->insert_id : false; + } + + /** + * Get execution logs for a schedule + * + * @param int $schedule_id Schedule ID + * @param array $args Query arguments + * @return array Array of log entries + */ + public function get_schedule_logs( $schedule_id, $args = array() ) { + global $wpdb; + + $defaults = array( + 'limit' => 50, + 'offset' => 0, + 'status' => null, + 'date_from' => null, + 'date_to' => null + ); + + $args = wp_parse_args( $args, $defaults ); + + $where_clauses = array( 'schedule_id = %d' ); + $where_values = array( intval( $schedule_id ) ); + + // Status filter + if ( ! empty( $args['status'] ) ) { + $where_clauses[] = 'status = %s'; + $where_values[] = $args['status']; + } + + // Date range filters + if ( ! empty( $args['date_from'] ) ) { + $where_clauses[] = 'sent_date >= %s'; + $where_values[] = $args['date_from']; + } + + if ( ! empty( $args['date_to'] ) ) { + $where_clauses[] = 'sent_date <= %s'; + $where_values[] = $args['date_to']; + } + + $where_sql = implode( ' AND ', $where_clauses ); + + $sql = "SELECT * FROM {$this->logs_table} + WHERE {$where_sql} + ORDER BY sent_date DESC + LIMIT %d OFFSET %d"; + + $where_values[] = intval( $args['limit'] ); + $where_values[] = intval( $args['offset'] ); + + $logs = $wpdb->get_results( $wpdb->prepare( $sql, $where_values ), ARRAY_A ); + + // Decode JSON details + foreach ( $logs as &$log ) { + if ( ! empty( $log['details'] ) ) { + $log['details'] = json_decode( $log['details'], true ); + } + } + + return $logs; + } + + /** + * Get logs for all schedules with filtering + * + * @param array $args Query arguments + * @return array Array of log entries with schedule info + */ + public function get_all_logs( $args = array() ) { + global $wpdb; + + $defaults = array( + 'limit' => 50, + 'offset' => 0, + 'trainer_id' => null, + 'status' => null, + 'date_from' => null, + 'date_to' => null + ); + + $args = wp_parse_args( $args, $defaults ); + + $schedules_table = $wpdb->prefix . 'hvac_communication_schedules'; + + $where_clauses = array(); + $where_values = array(); + + // Trainer filter + if ( ! empty( $args['trainer_id'] ) ) { + $where_clauses[] = 's.trainer_id = %d'; + $where_values[] = intval( $args['trainer_id'] ); + } + + // Status filter + if ( ! empty( $args['status'] ) ) { + $where_clauses[] = 'l.status = %s'; + $where_values[] = $args['status']; + } + + // Date range filters + if ( ! empty( $args['date_from'] ) ) { + $where_clauses[] = 'l.sent_date >= %s'; + $where_values[] = $args['date_from']; + } + + if ( ! empty( $args['date_to'] ) ) { + $where_clauses[] = 'l.sent_date <= %s'; + $where_values[] = $args['date_to']; + } + + $where_sql = ! empty( $where_clauses ) ? 'WHERE ' . implode( ' AND ', $where_clauses ) : ''; + + $sql = "SELECT l.*, + s.trainer_id, + s.event_id, + s.template_id, + s.trigger_type, + e.post_title as event_name, + t.post_title as template_name + FROM {$this->logs_table} l + LEFT JOIN {$schedules_table} s ON l.schedule_id = s.schedule_id + LEFT JOIN {$wpdb->posts} e ON s.event_id = e.ID + LEFT JOIN {$wpdb->posts} t ON s.template_id = t.ID + {$where_sql} + ORDER BY l.sent_date DESC + LIMIT %d OFFSET %d"; + + $where_values[] = intval( $args['limit'] ); + $where_values[] = intval( $args['offset'] ); + + $logs = $wpdb->get_results( $wpdb->prepare( $sql, $where_values ), ARRAY_A ); + + // Decode JSON details + foreach ( $logs as &$log ) { + if ( ! empty( $log['details'] ) ) { + $log['details'] = json_decode( $log['details'], true ); + } + } + + return $logs; + } + + /** + * Get summary statistics for communication logs + * + * @param int|null $trainer_id Optional trainer ID filter + * @param string|null $date_from Optional start date filter + * @param string|null $date_to Optional end date filter + * @return array Statistics array + */ + public function get_statistics( $trainer_id = null, $date_from = null, $date_to = null ) { + global $wpdb; + + $schedules_table = $wpdb->prefix . 'hvac_communication_schedules'; + + $where_clauses = array(); + $where_values = array(); + + if ( ! empty( $trainer_id ) ) { + $where_clauses[] = 's.trainer_id = %d'; + $where_values[] = intval( $trainer_id ); + } + + if ( ! empty( $date_from ) ) { + $where_clauses[] = 'l.sent_date >= %s'; + $where_values[] = $date_from; + } + + if ( ! empty( $date_to ) ) { + $where_clauses[] = 'l.sent_date <= %s'; + $where_values[] = $date_to; + } + + $where_sql = ! empty( $where_clauses ) ? 'WHERE ' . implode( ' AND ', $where_clauses ) : ''; + + $sql = "SELECT + COUNT(*) as total_executions, + COUNT(CASE WHEN l.status = 'sent' THEN 1 END) as successful_executions, + COUNT(CASE WHEN l.status = 'failed' THEN 1 END) as failed_executions, + COUNT(CASE WHEN l.status = 'skipped' THEN 1 END) as skipped_executions, + SUM(l.recipient_count) as total_recipients, + SUM(l.success_count) as total_emails_sent, + SUM(l.error_count) as total_email_errors, + AVG(l.execution_time) as avg_execution_time + FROM {$this->logs_table} l + LEFT JOIN {$schedules_table} s ON l.schedule_id = s.schedule_id + {$where_sql}"; + + $stats = $wpdb->get_row( + empty( $where_values ) ? $sql : $wpdb->prepare( $sql, $where_values ), + ARRAY_A + ); + + // Ensure numeric values + foreach ( $stats as $key => $value ) { + if ( in_array( $key, array( 'avg_execution_time' ) ) ) { + $stats[$key] = floatval( $value ); + } else { + $stats[$key] = intval( $value ); + } + } + + return $stats; + } + + /** + * Get recent execution activity + * + * @param int $limit Number of recent activities to retrieve + * @param int|null $trainer_id Optional trainer ID filter + * @return array Array of recent activities + */ + public function get_recent_activity( $limit = 10, $trainer_id = null ) { + global $wpdb; + + $schedules_table = $wpdb->prefix . 'hvac_communication_schedules'; + + $where_clause = ''; + $where_values = array(); + + if ( ! empty( $trainer_id ) ) { + $where_clause = 'WHERE s.trainer_id = %d'; + $where_values[] = intval( $trainer_id ); + } + + $sql = "SELECT l.*, + s.trainer_id, + e.post_title as event_name, + t.post_title as template_name + FROM {$this->logs_table} l + LEFT JOIN {$schedules_table} s ON l.schedule_id = s.schedule_id + LEFT JOIN {$wpdb->posts} e ON s.event_id = e.ID + LEFT JOIN {$wpdb->posts} t ON s.template_id = t.ID + {$where_clause} + ORDER BY l.sent_date DESC + LIMIT %d"; + + $where_values[] = intval( $limit ); + + $activities = $wpdb->get_results( $wpdb->prepare( $sql, $where_values ), ARRAY_A ); + + // Decode JSON details and format for display + foreach ( $activities as &$activity ) { + if ( ! empty( $activity['details'] ) ) { + $activity['details'] = json_decode( $activity['details'], true ); + } + + // Add human-readable time + $activity['time_ago'] = human_time_diff( strtotime( $activity['sent_date'] ), current_time( 'timestamp' ) ) . ' ago'; + } + + return $activities; + } + + /** + * Clean up old log entries + * + * @param int $days_to_keep Number of days to keep logs (default 90) + * @return int Number of entries deleted + */ + public function cleanup_old_logs( $days_to_keep = 90 ) { + global $wpdb; + + $cutoff_date = date( 'Y-m-d H:i:s', strtotime( "-{$days_to_keep} days" ) ); + + $deleted = $wpdb->query( $wpdb->prepare( + "DELETE FROM {$this->logs_table} WHERE sent_date < %s", + $cutoff_date + ) ); + + if ( $deleted !== false && class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::info( "Cleaned up {$deleted} old communication log entries", 'Communication Logger' ); + } + + return intval( $deleted ); + } + + /** + * Export logs to CSV format + * + * @param array $args Export arguments + * @return string CSV content + */ + public function export_logs_csv( $args = array() ) { + $logs = $this->get_all_logs( $args ); + + $csv_header = array( + 'Date', + 'Schedule ID', + 'Event', + 'Template', + 'Status', + 'Recipients', + 'Successful', + 'Errors', + 'Execution Time (s)' + ); + + $csv_data = array(); + $csv_data[] = $csv_header; + + foreach ( $logs as $log ) { + $csv_data[] = array( + $log['sent_date'], + $log['schedule_id'], + $log['event_name'] ?: 'N/A', + $log['template_name'] ?: 'N/A', + $log['status'], + $log['recipient_count'], + $log['success_count'], + $log['error_count'], + $log['execution_time'] + ); + } + + // Convert to CSV string + $csv_content = ''; + foreach ( $csv_data as $row ) { + $csv_content .= '"' . implode( '","', $row ) . '"' . "\n"; + } + + return $csv_content; + } + + /** + * Get performance metrics for schedules + * + * @param int|null $trainer_id Optional trainer ID filter + * @return array Performance metrics + */ + public function get_performance_metrics( $trainer_id = null ) { + global $wpdb; + + $schedules_table = $wpdb->prefix . 'hvac_communication_schedules'; + + $where_clause = ''; + $where_values = array(); + + if ( ! empty( $trainer_id ) ) { + $where_clause = 'WHERE s.trainer_id = %d'; + $where_values[] = intval( $trainer_id ); + } + + // Get delivery success rate + $sql = "SELECT + COUNT(*) as total_schedules, + AVG(CASE WHEN l.status = 'sent' THEN 100.0 ELSE 0.0 END) as success_rate, + AVG(l.execution_time) as avg_execution_time, + SUM(l.recipient_count) / COUNT(*) as avg_recipients_per_execution + FROM {$this->logs_table} l + LEFT JOIN {$schedules_table} s ON l.schedule_id = s.schedule_id + {$where_clause}"; + + $metrics = $wpdb->get_row( + empty( $where_values ) ? $sql : $wpdb->prepare( $sql, $where_values ), + ARRAY_A + ); + + // Format metrics + $metrics['success_rate'] = round( floatval( $metrics['success_rate'] ), 2 ); + $metrics['avg_execution_time'] = round( floatval( $metrics['avg_execution_time'] ), 3 ); + $metrics['avg_recipients_per_execution'] = round( floatval( $metrics['avg_recipients_per_execution'] ), 1 ); + + return $metrics; + } +} \ No newline at end of file diff --git a/includes/communication/class-communication-schedule-manager.php b/includes/communication/class-communication-schedule-manager.php new file mode 100644 index 00000000..c5b54b91 --- /dev/null +++ b/includes/communication/class-communication-schedule-manager.php @@ -0,0 +1,603 @@ +schedules_table = $wpdb->prefix . 'hvac_communication_schedules'; + $this->logs_table = $wpdb->prefix . 'hvac_communication_logs'; + $this->tracking_table = $wpdb->prefix . 'hvac_event_communication_tracking'; + } + + /** + * Save a communication schedule + * + * @param array $schedule_data Schedule configuration + * @return int|false Schedule ID on success, false on failure + */ + public function save_schedule( $schedule_data ) { + global $wpdb; + + $data = array( + 'trainer_id' => intval( $schedule_data['trainer_id'] ), + 'event_id' => ! empty( $schedule_data['event_id'] ) ? intval( $schedule_data['event_id'] ) : null, + 'template_id' => intval( $schedule_data['template_id'] ), + 'schedule_type' => isset( $schedule_data['schedule_type'] ) ? $schedule_data['schedule_type'] : 'time_based', + 'trigger_type' => sanitize_text_field( $schedule_data['trigger_type'] ), + 'trigger_value' => intval( $schedule_data['trigger_value'] ), + 'trigger_unit' => sanitize_text_field( $schedule_data['trigger_unit'] ), + 'status' => isset( $schedule_data['status'] ) ? sanitize_text_field( $schedule_data['status'] ) : 'active', + 'target_audience' => sanitize_text_field( $schedule_data['target_audience'] ), + 'custom_recipient_list' => ! empty( $schedule_data['custom_recipient_list'] ) ? + sanitize_textarea_field( $schedule_data['custom_recipient_list'] ) : null, + 'conditions' => ! empty( $schedule_data['conditions'] ) ? + wp_json_encode( $schedule_data['conditions'] ) : null, + 'next_run' => ! empty( $schedule_data['next_run'] ) ? + sanitize_text_field( $schedule_data['next_run'] ) : null, + 'is_recurring' => isset( $schedule_data['is_recurring'] ) ? + (int) $schedule_data['is_recurring'] : 0, + 'recurring_interval' => ! empty( $schedule_data['recurring_interval'] ) ? + intval( $schedule_data['recurring_interval'] ) : null, + 'recurring_unit' => ! empty( $schedule_data['recurring_unit'] ) ? + sanitize_text_field( $schedule_data['recurring_unit'] ) : null, + 'max_runs' => ! empty( $schedule_data['max_runs'] ) ? + intval( $schedule_data['max_runs'] ) : null + ); + + $formats = array( + '%d', // trainer_id + '%d', // event_id + '%d', // template_id + '%s', // schedule_type + '%s', // trigger_type + '%d', // trigger_value + '%s', // trigger_unit + '%s', // status + '%s', // target_audience + '%s', // custom_recipient_list + '%s', // conditions + '%s', // next_run + '%d', // is_recurring + '%d', // recurring_interval + '%s', // recurring_unit + '%d' // max_runs + ); + + $result = $wpdb->insert( $this->schedules_table, $data, $formats ); + + if ( $result === false ) { + if ( class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::error( 'Failed to save communication schedule: ' . $wpdb->last_error, 'Schedule Manager' ); + } + return false; + } + + return $wpdb->insert_id; + } + + /** + * Update a communication schedule + * + * @param int $schedule_id Schedule ID + * @param array $schedule_data Updated schedule data + * @return bool Success status + */ + public function update_schedule( $schedule_id, $schedule_data ) { + global $wpdb; + + $data = array(); + $formats = array(); + + // Only update provided fields + $allowed_fields = array( + 'event_id' => '%d', + 'template_id' => '%d', + 'schedule_type' => '%s', + 'trigger_type' => '%s', + 'trigger_value' => '%d', + 'trigger_unit' => '%s', + 'status' => '%s', + 'target_audience' => '%s', + 'custom_recipient_list' => '%s', + 'conditions' => '%s', + 'next_run' => '%s', + 'is_recurring' => '%d', + 'recurring_interval' => '%d', + 'recurring_unit' => '%s', + 'max_runs' => '%d' + ); + + foreach ( $allowed_fields as $field => $format ) { + if ( array_key_exists( $field, $schedule_data ) ) { + if ( $field === 'conditions' && ! empty( $schedule_data[$field] ) ) { + $data[$field] = wp_json_encode( $schedule_data[$field] ); + } elseif ( in_array( $format, array( '%d' ) ) ) { + $data[$field] = intval( $schedule_data[$field] ); + } else { + $data[$field] = sanitize_text_field( $schedule_data[$field] ); + } + $formats[] = $format; + } + } + + // Add modified timestamp + $data['modified_date'] = current_time( 'mysql' ); + $formats[] = '%s'; + + $result = $wpdb->update( + $this->schedules_table, + $data, + array( 'schedule_id' => intval( $schedule_id ) ), + $formats, + array( '%d' ) + ); + + return $result !== false; + } + + /** + * Get a communication schedule by ID + * + * @param int $schedule_id Schedule ID + * @return array|null Schedule data or null if not found + */ + public function get_schedule( $schedule_id ) { + global $wpdb; + + $schedule = $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$this->schedules_table} WHERE schedule_id = %d", + intval( $schedule_id ) + ), ARRAY_A ); + + if ( $schedule && ! empty( $schedule['conditions'] ) ) { + $schedule['conditions'] = json_decode( $schedule['conditions'], true ); + } + + return $schedule; + } + + /** + * Get schedules by trainer + * + * @param int $trainer_id Trainer user ID + * @param int $event_id Optional specific event ID + * @return array Array of schedules + */ + public function get_schedules_by_trainer( $trainer_id, $event_id = null ) { + global $wpdb; + + $where_clause = "WHERE trainer_id = %d"; + $params = array( intval( $trainer_id ) ); + + if ( $event_id ) { + $where_clause .= " AND event_id = %d"; + $params[] = intval( $event_id ); + } + + $schedules = $wpdb->get_results( $wpdb->prepare( + "SELECT s.*, + t.post_title as template_name, + e.post_title as event_name, + e.post_status as event_status + FROM {$this->schedules_table} s + LEFT JOIN {$wpdb->posts} t ON s.template_id = t.ID + LEFT JOIN {$wpdb->posts} e ON s.event_id = e.ID + {$where_clause} + ORDER BY s.created_date DESC", + $params + ), ARRAY_A ); + + // Process conditions field + foreach ( $schedules as &$schedule ) { + if ( ! empty( $schedule['conditions'] ) ) { + $schedule['conditions'] = json_decode( $schedule['conditions'], true ); + } + } + + return $schedules; + } + + /** + * Get schedules by event + * + * @param int $event_id Event ID + * @return array Array of schedules + */ + public function get_schedules_by_event( $event_id ) { + global $wpdb; + + $schedules = $wpdb->get_results( $wpdb->prepare( + "SELECT s.*, + t.post_title as template_name + FROM {$this->schedules_table} s + LEFT JOIN {$wpdb->posts} t ON s.template_id = t.ID + WHERE s.event_id = %d + ORDER BY s.trigger_type, s.trigger_value", + intval( $event_id ) + ), ARRAY_A ); + + // Process conditions field + foreach ( $schedules as &$schedule ) { + if ( ! empty( $schedule['conditions'] ) ) { + $schedule['conditions'] = json_decode( $schedule['conditions'], true ); + } + } + + return $schedules; + } + + /** + * Get active schedules + * + * @return array Array of active schedules + */ + public function get_active_schedules() { + global $wpdb; + + return $wpdb->get_results( + "SELECT * FROM {$this->schedules_table} + WHERE status = 'active' + ORDER BY next_run ASC", + ARRAY_A + ); + } + + /** + * Get due schedules + * + * @return array Array of schedules that are due for execution + */ + public function get_due_schedules() { + global $wpdb; + + $current_time = current_time( 'mysql' ); + + $schedules = $wpdb->get_results( $wpdb->prepare( + "SELECT * FROM {$this->schedules_table} + WHERE status = 'active' + AND next_run IS NOT NULL + AND next_run <= %s + AND (max_runs IS NULL OR run_count < max_runs) + ORDER BY next_run ASC", + $current_time + ), ARRAY_A ); + + return $schedules; + } + + /** + * Delete a communication schedule + * + * @param int $schedule_id Schedule ID + * @return bool Success status + */ + public function delete_schedule( $schedule_id ) { + global $wpdb; + + // Delete associated logs first (foreign key constraint) + $wpdb->delete( + $this->logs_table, + array( 'schedule_id' => intval( $schedule_id ) ), + array( '%d' ) + ); + + // Delete the schedule + $result = $wpdb->delete( + $this->schedules_table, + array( 'schedule_id' => intval( $schedule_id ) ), + array( '%d' ) + ); + + return $result !== false; + } + + /** + * Update schedule run tracking + * + * @param int $schedule_id Schedule ID + * @return bool Success status + */ + public function update_schedule_run_tracking( $schedule_id ) { + global $wpdb; + + $schedule = $this->get_schedule( $schedule_id ); + if ( ! $schedule ) { + return false; + } + + $data = array( + 'last_run' => current_time( 'mysql' ), + 'run_count' => intval( $schedule['run_count'] ) + 1 + ); + + // Calculate next run if recurring + if ( $schedule['is_recurring'] ) { + $next_run = $this->calculate_next_recurring_run( $schedule ); + if ( $next_run ) { + $data['next_run'] = $next_run; + } + } else { + // Mark as completed if not recurring + $data['status'] = 'completed'; + $data['next_run'] = null; + } + + return $this->update_schedule( $schedule_id, $data ); + } + + /** + * Calculate next recurring run time + * + * @param array $schedule Schedule data + * @return string|null Next run time or null + */ + private function calculate_next_recurring_run( $schedule ) { + if ( ! $schedule['is_recurring'] || ! $schedule['recurring_interval'] ) { + return null; + } + + $interval = $schedule['recurring_interval']; + $unit = $schedule['recurring_unit']; + + $current_time = current_time( 'timestamp' ); + + switch ( $unit ) { + case 'days': + $next_time = $current_time + ( $interval * DAY_IN_SECONDS ); + break; + case 'weeks': + $next_time = $current_time + ( $interval * WEEK_IN_SECONDS ); + break; + case 'months': + $next_time = strtotime( "+{$interval} months", $current_time ); + break; + default: + return null; + } + + return date( 'Y-m-d H:i:s', $next_time ); + } + + /** + * Validate schedule data + * + * @param array $schedule_data Schedule data to validate + * @return bool|WP_Error True if valid, WP_Error if invalid + */ + public function validate_schedule_data( $schedule_data ) { + // Required fields + $required_fields = array( 'trainer_id', 'template_id', 'trigger_type', 'target_audience' ); + + foreach ( $required_fields as $field ) { + if ( empty( $schedule_data[$field] ) ) { + return new WP_Error( 'missing_field', sprintf( __( 'Required field missing: %s', 'hvac-community-events' ), $field ) ); + } + } + + // Validate trainer exists and has permission + $trainer = get_user_by( 'id', $schedule_data['trainer_id'] ); + if ( ! $trainer || ! in_array( 'hvac_trainer', $trainer->roles ) ) { + return new WP_Error( 'invalid_trainer', __( 'Invalid trainer specified.', 'hvac-community-events' ) ); + } + + // Validate template exists and belongs to trainer + $template = get_post( $schedule_data['template_id'] ); + if ( ! $template || $template->post_type !== 'hvac_email_template' ) { + return new WP_Error( 'invalid_template', __( 'Invalid template specified.', 'hvac-community-events' ) ); + } + + if ( $template->post_author != $schedule_data['trainer_id'] && ! current_user_can( 'edit_others_posts' ) ) { + return new WP_Error( 'template_permission', __( 'You do not have permission to use this template.', 'hvac-community-events' ) ); + } + + // Validate event if specified + if ( ! empty( $schedule_data['event_id'] ) ) { + $event = get_post( $schedule_data['event_id'] ); + if ( ! $event || $event->post_type !== 'tribe_events' ) { + return new WP_Error( 'invalid_event', __( 'Invalid event specified.', 'hvac-community-events' ) ); + } + + // Check if trainer owns the event + if ( $event->post_author != $schedule_data['trainer_id'] && ! current_user_can( 'edit_others_posts' ) ) { + return new WP_Error( 'event_permission', __( 'You do not have permission to schedule communications for this event.', 'hvac-community-events' ) ); + } + } + + // Validate trigger settings + $valid_trigger_types = array( 'before_event', 'after_event', 'on_registration', 'custom_date' ); + if ( ! in_array( $schedule_data['trigger_type'], $valid_trigger_types ) ) { + return new WP_Error( 'invalid_trigger_type', __( 'Invalid trigger type specified.', 'hvac-community-events' ) ); + } + + $valid_trigger_units = array( 'minutes', 'hours', 'days', 'weeks' ); + if ( ! in_array( $schedule_data['trigger_unit'], $valid_trigger_units ) ) { + return new WP_Error( 'invalid_trigger_unit', __( 'Invalid trigger unit specified.', 'hvac-community-events' ) ); + } + + // Validate audience settings + $valid_audiences = array( 'all_attendees', 'confirmed_attendees', 'pending_attendees', 'custom_list' ); + if ( ! in_array( $schedule_data['target_audience'], $valid_audiences ) ) { + return new WP_Error( 'invalid_audience', __( 'Invalid target audience specified.', 'hvac-community-events' ) ); + } + + // Validate custom recipient list if specified + if ( $schedule_data['target_audience'] === 'custom_list' && empty( $schedule_data['custom_recipient_list'] ) ) { + return new WP_Error( 'missing_recipients', __( 'Custom recipient list is required when target audience is set to custom list.', 'hvac-community-events' ) ); + } + + // Validate recurring settings + if ( ! empty( $schedule_data['is_recurring'] ) ) { + if ( empty( $schedule_data['recurring_interval'] ) || empty( $schedule_data['recurring_unit'] ) ) { + return new WP_Error( 'invalid_recurring', __( 'Recurring interval and unit are required for recurring schedules.', 'hvac-community-events' ) ); + } + + $valid_recurring_units = array( 'days', 'weeks', 'months' ); + if ( ! in_array( $schedule_data['recurring_unit'], $valid_recurring_units ) ) { + return new WP_Error( 'invalid_recurring_unit', __( 'Invalid recurring unit specified.', 'hvac-community-events' ) ); + } + } + + return true; + } + + /** + * Check for schedule conflicts + * + * @param array $schedule_data Schedule data to check + * @return bool|WP_Error True if no conflicts, WP_Error if conflicts found + */ + public function check_schedule_conflicts( $schedule_data ) { + global $wpdb; + + // Check for duplicate schedules with same trigger settings + $existing = $wpdb->get_var( $wpdb->prepare( + "SELECT COUNT(*) FROM {$this->schedules_table} + WHERE trainer_id = %d + AND event_id = %d + AND template_id = %d + AND trigger_type = %s + AND trigger_value = %d + AND trigger_unit = %s + AND status = 'active'", + $schedule_data['trainer_id'], + $schedule_data['event_id'] ?? 0, + $schedule_data['template_id'], + $schedule_data['trigger_type'], + $schedule_data['trigger_value'], + $schedule_data['trigger_unit'] + ) ); + + if ( $existing > 0 ) { + return new WP_Error( 'duplicate_schedule', __( 'A schedule with identical settings already exists.', 'hvac-community-events' ) ); + } + + return true; + } + + /** + * Check if user can edit schedule + * + * @param int $schedule_id Schedule ID + * @return bool Whether user can edit the schedule + */ + public function user_can_edit_schedule( $schedule_id ) { + $schedule = $this->get_schedule( $schedule_id ); + + if ( ! $schedule ) { + return false; + } + + $current_user_id = get_current_user_id(); + + // Owner can edit + if ( $schedule['trainer_id'] == $current_user_id ) { + return true; + } + + // Admins can edit others' schedules + if ( current_user_can( 'edit_others_posts' ) ) { + return true; + } + + return false; + } + + /** + * Get available templates for scheduling + * + * @param int $trainer_id Trainer user ID + * @return array Array of available templates + */ + public function get_available_templates( $trainer_id ) { + $templates_manager = new HVAC_Communication_Templates(); + return $templates_manager->get_user_templates( $trainer_id ); + } + + /** + * Validate template compatibility with schedule type + * + * @param int $template_id Template ID + * @param string $schedule_type Schedule type + * @return bool|WP_Error True if compatible, WP_Error if not + */ + public function validate_template_compatibility( $template_id, $schedule_type ) { + $template = get_post( $template_id ); + + if ( ! $template || $template->post_type !== 'hvac_email_template' ) { + return new WP_Error( 'invalid_template', __( 'Invalid template specified.', 'hvac-community-events' ) ); + } + + // Check for required placeholders based on schedule type + $required_placeholders = array( '{attendee_name}', '{event_title}' ); + + if ( $schedule_type === 'event_based' ) { + $required_placeholders[] = '{event_date}'; + $required_placeholders[] = '{event_time}'; + } + + foreach ( $required_placeholders as $placeholder ) { + if ( strpos( $template->post_content, $placeholder ) === false ) { + return new WP_Error( 'missing_placeholder', + sprintf( __( 'Template missing required placeholder: %s', 'hvac-community-events' ), $placeholder ) + ); + } + } + + return true; + } + + /** + * Get schedule statistics for a trainer + * + * @param int $trainer_id Trainer user ID + * @return array Statistics array + */ + public function get_trainer_schedule_stats( $trainer_id ) { + global $wpdb; + + $stats = $wpdb->get_row( $wpdb->prepare( + "SELECT + COUNT(*) as total_schedules, + COUNT(CASE WHEN status = 'active' THEN 1 END) as active_schedules, + COUNT(CASE WHEN status = 'paused' THEN 1 END) as paused_schedules, + COUNT(CASE WHEN status = 'completed' THEN 1 END) as completed_schedules, + SUM(run_count) as total_executions + FROM {$this->schedules_table} + WHERE trainer_id = %d", + $trainer_id + ), ARRAY_A ); + + return $stats; + } +} \ No newline at end of file diff --git a/includes/communication/class-communication-scheduler.php b/includes/communication/class-communication-scheduler.php new file mode 100644 index 00000000..b4cf11d5 --- /dev/null +++ b/includes/communication/class-communication-scheduler.php @@ -0,0 +1,596 @@ +init_dependencies(); + $this->register_hooks(); + + // Debug logging + if ( class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::info( 'HVAC_Communication_Scheduler initialized', 'Scheduler' ); + } + } + + /** + * Initialize dependencies + */ + private function init_dependencies() { + require_once HVAC_PLUGIN_DIR . 'includes/communication/class-communication-schedule-manager.php'; + require_once HVAC_PLUGIN_DIR . 'includes/communication/class-communication-trigger-engine.php'; + require_once HVAC_PLUGIN_DIR . 'includes/communication/class-communication-logger.php'; + + $this->schedule_manager = new HVAC_Communication_Schedule_Manager(); + $this->trigger_engine = new HVAC_Communication_Trigger_Engine(); + $this->logger = new HVAC_Communication_Logger(); + } + + /** + * Register WordPress hooks + */ + private function register_hooks() { + // Cron hooks + add_action( 'hvac_process_communication_schedules', array( $this, 'process_scheduled_communications' ) ); + + // Event-based triggers + add_action( 'tribe_events_event_save_after', array( $this, 'on_event_saved' ) ); + add_action( 'event_tickets_after_add_attendee', array( $this, 'on_attendee_registered' ) ); + add_action( 'wp', array( $this, 'check_event_date_changes' ) ); + + // AJAX handlers + add_action( 'wp_ajax_hvac_create_schedule', array( $this, 'ajax_create_schedule' ) ); + add_action( 'wp_ajax_hvac_update_schedule', array( $this, 'ajax_update_schedule' ) ); + add_action( 'wp_ajax_hvac_delete_schedule', array( $this, 'ajax_delete_schedule' ) ); + add_action( 'wp_ajax_hvac_get_schedules', array( $this, 'ajax_get_schedules' ) ); + add_action( 'wp_ajax_hvac_toggle_schedule', array( $this, 'ajax_toggle_schedule' ) ); + add_action( 'wp_ajax_hvac_preview_recipients', array( $this, 'ajax_preview_recipients' ) ); + + // Custom cron schedules + add_filter( 'cron_schedules', array( $this, 'add_custom_cron_schedules' ) ); + + // Initialize cron if not scheduled + add_action( 'wp_loaded', array( $this, 'setup_cron_schedules' ) ); + } + + /** + * Add custom cron schedules + */ + public function add_custom_cron_schedules( $schedules ) { + $schedules['hvac_every_5_minutes'] = array( + 'interval' => 300, + 'display' => __( 'Every 5 minutes', 'hvac-community-events' ) + ); + + $schedules['hvac_every_15_minutes'] = array( + 'interval' => 900, + 'display' => __( 'Every 15 minutes', 'hvac-community-events' ) + ); + + return $schedules; + } + + /** + * Setup cron schedules + */ + public function setup_cron_schedules() { + if ( ! wp_next_scheduled( 'hvac_process_communication_schedules' ) ) { + wp_schedule_event( time(), 'hvac_every_15_minutes', 'hvac_process_communication_schedules' ); + + if ( class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::info( 'Communication scheduler cron job set up', 'Scheduler' ); + } + } + } + + /** + * Create a new communication schedule + * + * @param array $schedule_data Schedule configuration + * @return int|WP_Error Schedule ID on success, WP_Error on failure + */ + public function create_schedule( $schedule_data ) { + // Validate schedule data + $validation_result = $this->schedule_manager->validate_schedule_data( $schedule_data ); + if ( is_wp_error( $validation_result ) ) { + return $validation_result; + } + + // Check for conflicts + $conflict_check = $this->schedule_manager->check_schedule_conflicts( $schedule_data ); + if ( is_wp_error( $conflict_check ) ) { + return $conflict_check; + } + + // Calculate next run time + $next_run = $this->calculate_next_run_time( $schedule_data ); + $schedule_data['next_run'] = $next_run; + + // Save schedule + $schedule_id = $this->schedule_manager->save_schedule( $schedule_data ); + + if ( $schedule_id && class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::info( "Communication schedule created: ID $schedule_id", 'Scheduler' ); + } + + return $schedule_id; + } + + /** + * Update an existing communication schedule + * + * @param int $schedule_id Schedule ID + * @param array $schedule_data Updated schedule configuration + * @return bool|WP_Error Success status + */ + public function update_schedule( $schedule_id, $schedule_data ) { + // Verify ownership + if ( ! $this->schedule_manager->user_can_edit_schedule( $schedule_id ) ) { + return new WP_Error( 'permission_denied', __( 'You do not have permission to edit this schedule.', 'hvac-community-events' ) ); + } + + // Validate data + $validation_result = $this->schedule_manager->validate_schedule_data( $schedule_data ); + if ( is_wp_error( $validation_result ) ) { + return $validation_result; + } + + // Recalculate next run time if trigger settings changed + $existing_schedule = $this->schedule_manager->get_schedule( $schedule_id ); + $trigger_changed = ( + $existing_schedule['trigger_type'] !== $schedule_data['trigger_type'] || + $existing_schedule['trigger_value'] !== $schedule_data['trigger_value'] || + $existing_schedule['trigger_unit'] !== $schedule_data['trigger_unit'] + ); + + if ( $trigger_changed ) { + $schedule_data['next_run'] = $this->calculate_next_run_time( $schedule_data ); + } + + return $this->schedule_manager->update_schedule( $schedule_id, $schedule_data ); + } + + /** + * Delete a communication schedule + * + * @param int $schedule_id Schedule ID + * @return bool|WP_Error Success status + */ + public function delete_schedule( $schedule_id ) { + // Verify ownership + if ( ! $this->schedule_manager->user_can_edit_schedule( $schedule_id ) ) { + return new WP_Error( 'permission_denied', __( 'You do not have permission to delete this schedule.', 'hvac-community-events' ) ); + } + + $result = $this->schedule_manager->delete_schedule( $schedule_id ); + + if ( $result && class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::info( "Communication schedule deleted: ID $schedule_id", 'Scheduler' ); + } + + return $result; + } + + /** + * Get schedules for a trainer + * + * @param int $trainer_id Trainer user ID + * @param int $event_id Optional specific event ID + * @return array Array of schedules + */ + public function get_trainer_schedules( $trainer_id, $event_id = null ) { + return $this->schedule_manager->get_schedules_by_trainer( $trainer_id, $event_id ); + } + + /** + * Process all due scheduled communications + */ + public function process_scheduled_communications() { + $due_schedules = $this->schedule_manager->get_due_schedules(); + + if ( class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::info( 'Processing ' . count( $due_schedules ) . ' due communication schedules', 'Scheduler' ); + } + + foreach ( $due_schedules as $schedule ) { + $this->execute_schedule( $schedule['schedule_id'] ); + } + } + + /** + * Calculate next run time for a schedule + * + * @param array $schedule Schedule configuration + * @return string MySQL datetime string + */ + public function calculate_next_run_time( $schedule ) { + if ( ! empty( $schedule['event_id'] ) ) { + // Event-based scheduling + $event_date = get_post_meta( $schedule['event_id'], '_EventStartDate', true ); + if ( ! $event_date ) { + return null; + } + + return $this->trigger_engine->calculate_trigger_time( $event_date, $schedule ); + } else { + // Immediate or custom date scheduling + if ( $schedule['trigger_type'] === 'custom_date' && ! empty( $schedule['custom_date'] ) ) { + return $schedule['custom_date']; + } elseif ( $schedule['trigger_type'] === 'on_registration' ) { + // This will be triggered immediately on registration + return null; + } + } + + return null; + } + + /** + * Execute a specific schedule + * + * @param int $schedule_id Schedule ID + * @return bool Success status + */ + public function execute_schedule( $schedule_id ) { + $schedule = $this->schedule_manager->get_schedule( $schedule_id ); + if ( ! $schedule ) { + return false; + } + + try { + // Get recipients + $recipients = $this->trigger_engine->get_schedule_recipients( $schedule ); + + if ( empty( $recipients ) ) { + $this->logger->log_schedule_execution( $schedule_id, 'skipped', array( + 'reason' => 'No recipients found' + ) ); + return true; + } + + // Execute communication + $result = $this->trigger_engine->execute_communication( $schedule, $recipients ); + + // Update schedule run tracking + $this->schedule_manager->update_schedule_run_tracking( $schedule_id ); + + // Log execution + $this->logger->log_schedule_execution( $schedule_id, 'sent', array( + 'recipient_count' => count( $recipients ), + 'success' => $result + ) ); + + return $result; + + } catch ( Exception $e ) { + $this->logger->log_schedule_execution( $schedule_id, 'failed', array( + 'error' => $e->getMessage() + ) ); + + if ( class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::error( "Schedule execution failed: " . $e->getMessage(), 'Scheduler' ); + } + + return false; + } + } + + /** + * Handle event saved/updated + * + * @param int $event_id Event ID + */ + public function on_event_saved( $event_id ) { + $schedules = $this->schedule_manager->get_schedules_by_event( $event_id ); + + foreach ( $schedules as $schedule ) { + // Recalculate next run time if event date changed + $new_next_run = $this->calculate_next_run_time( $schedule ); + + if ( $new_next_run !== $schedule['next_run'] ) { + $this->schedule_manager->update_schedule( $schedule['schedule_id'], array( + 'next_run' => $new_next_run + ) ); + } + } + } + + /** + * Handle attendee registration + * + * @param int $attendee_id Attendee ID + * @param int $event_id Event ID + */ + public function on_attendee_registered( $attendee_id, $event_id ) { + // Process immediate registration triggers + $this->trigger_engine->process_registration_triggers( $attendee_id, $event_id ); + } + + /** + * Check for event date changes + */ + public function check_event_date_changes() { + // This will be called on wp hook to check for any event date changes + // and update corresponding schedules + if ( ! is_admin() || ! current_user_can( 'edit_posts' ) ) { + return; + } + + // Process any date change updates + $this->trigger_engine->process_event_date_changes(); + } + + /** + * AJAX: Create schedule + */ + public function ajax_create_schedule() { + check_ajax_referer( 'hvac_scheduler_nonce', 'nonce' ); + + if ( ! is_user_logged_in() ) { + wp_send_json_error( array( 'message' => __( 'You must be logged in to create schedules.', 'hvac-community-events' ) ) ); + } + + $schedule_data = $this->sanitize_schedule_data( $_POST ); + $schedule_data['trainer_id'] = get_current_user_id(); + + $result = $this->create_schedule( $schedule_data ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ) ); + } + + wp_send_json_success( array( + 'schedule_id' => $result, + 'message' => __( 'Schedule created successfully.', 'hvac-community-events' ) + ) ); + } + + /** + * AJAX: Update schedule + */ + public function ajax_update_schedule() { + check_ajax_referer( 'hvac_scheduler_nonce', 'nonce' ); + + if ( ! is_user_logged_in() ) { + wp_send_json_error( array( 'message' => __( 'You must be logged in to update schedules.', 'hvac-community-events' ) ) ); + } + + $schedule_id = intval( $_POST['schedule_id'] ); + $schedule_data = $this->sanitize_schedule_data( $_POST ); + + $result = $this->update_schedule( $schedule_id, $schedule_data ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ) ); + } + + wp_send_json_success( array( 'message' => __( 'Schedule updated successfully.', 'hvac-community-events' ) ) ); + } + + /** + * AJAX: Delete schedule + */ + public function ajax_delete_schedule() { + check_ajax_referer( 'hvac_scheduler_nonce', 'nonce' ); + + if ( ! is_user_logged_in() ) { + wp_send_json_error( array( 'message' => __( 'You must be logged in to delete schedules.', 'hvac-community-events' ) ) ); + } + + $schedule_id = intval( $_POST['schedule_id'] ); + $result = $this->delete_schedule( $schedule_id ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ) ); + } + + wp_send_json_success( array( 'message' => __( 'Schedule deleted successfully.', 'hvac-community-events' ) ) ); + } + + /** + * AJAX: Get schedules + */ + public function ajax_get_schedules() { + check_ajax_referer( 'hvac_scheduler_nonce', 'nonce' ); + + if ( ! is_user_logged_in() ) { + wp_send_json_error( array( 'message' => __( 'You must be logged in to view schedules.', 'hvac-community-events' ) ) ); + } + + $trainer_id = get_current_user_id(); + $event_id = isset( $_POST['event_id'] ) ? intval( $_POST['event_id'] ) : null; + $status_filter = isset( $_POST['status'] ) ? sanitize_text_field( $_POST['status'] ) : null; + + $schedules = $this->get_trainer_schedules( $trainer_id, $event_id ); + + if ( $status_filter && $status_filter !== 'all' ) { + $schedules = array_filter( $schedules, function( $schedule ) use ( $status_filter ) { + return $schedule['status'] === $status_filter; + } ); + } + + wp_send_json_success( array( 'schedules' => array_values( $schedules ) ) ); + } + + /** + * AJAX: Toggle schedule status + */ + public function ajax_toggle_schedule() { + check_ajax_referer( 'hvac_scheduler_nonce', 'nonce' ); + + if ( ! is_user_logged_in() ) { + wp_send_json_error( array( 'message' => __( 'You must be logged in to toggle schedules.', 'hvac-community-events' ) ) ); + } + + $schedule_id = intval( $_POST['schedule_id'] ); + $schedule = $this->schedule_manager->get_schedule( $schedule_id ); + + if ( ! $schedule ) { + wp_send_json_error( array( 'message' => __( 'Schedule not found.', 'hvac-community-events' ) ) ); + } + + $new_status = ( $schedule['status'] === 'active' ) ? 'paused' : 'active'; + + $result = $this->update_schedule( $schedule_id, array( 'status' => $new_status ) ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ) ); + } + + wp_send_json_success( array( + 'status' => $new_status, + 'message' => sprintf( __( 'Schedule %s.', 'hvac-community-events' ), $new_status ) + ) ); + } + + /** + * AJAX: Preview recipients + */ + public function ajax_preview_recipients() { + check_ajax_referer( 'hvac_scheduler_nonce', 'nonce' ); + + if ( ! is_user_logged_in() ) { + wp_send_json_error( array( 'message' => __( 'You must be logged in to preview recipients.', 'hvac-community-events' ) ) ); + } + + $schedule_data = $this->sanitize_schedule_data( $_POST ); + $schedule_data['trainer_id'] = get_current_user_id(); + + $recipients = $this->trigger_engine->get_schedule_recipients( $schedule_data ); + + wp_send_json_success( array( + 'recipients' => $recipients, + 'count' => count( $recipients ) + ) ); + } + + /** + * Sanitize schedule data from form input + * + * @param array $raw_data Raw POST data + * @return array Sanitized schedule data + */ + private function sanitize_schedule_data( $raw_data ) { + return array( + 'schedule_name' => isset( $raw_data['schedule_name'] ) ? sanitize_text_field( $raw_data['schedule_name'] ) : '', + 'event_id' => isset( $raw_data['event_id'] ) ? intval( $raw_data['event_id'] ) : null, + 'template_id' => isset( $raw_data['template_id'] ) ? intval( $raw_data['template_id'] ) : 0, + 'trigger_type' => isset( $raw_data['trigger_type'] ) ? sanitize_text_field( $raw_data['trigger_type'] ) : '', + 'trigger_value' => isset( $raw_data['trigger_value'] ) ? intval( $raw_data['trigger_value'] ) : 0, + 'trigger_unit' => isset( $raw_data['trigger_unit'] ) ? sanitize_text_field( $raw_data['trigger_unit'] ) : 'days', + 'target_audience' => isset( $raw_data['target_audience'] ) ? sanitize_text_field( $raw_data['target_audience'] ) : 'all_attendees', + 'custom_recipient_list' => isset( $raw_data['custom_recipient_list'] ) ? sanitize_textarea_field( $raw_data['custom_recipient_list'] ) : '', + 'is_recurring' => isset( $raw_data['is_recurring'] ) ? (bool) $raw_data['is_recurring'] : false, + 'recurring_interval' => isset( $raw_data['recurring_interval'] ) ? intval( $raw_data['recurring_interval'] ) : null, + 'recurring_unit' => isset( $raw_data['recurring_unit'] ) ? sanitize_text_field( $raw_data['recurring_unit'] ) : null, + 'max_runs' => isset( $raw_data['max_runs'] ) ? intval( $raw_data['max_runs'] ) : null, + 'status' => isset( $raw_data['status'] ) ? sanitize_text_field( $raw_data['status'] ) : 'active' + ); + } + + /** + * Get default schedule templates + * + * @return array Default schedule configurations + */ + public function get_default_schedule_templates() { + return array( + 'event_reminder_24h' => array( + 'name' => __( '24-Hour Event Reminder', 'hvac-community-events' ), + 'trigger_type' => 'before_event', + 'trigger_value' => 1, + 'trigger_unit' => 'days', + 'template_category' => 'event_reminder', + 'target_audience' => 'confirmed_attendees', + 'description' => __( 'Send reminder 24 hours before event starts', 'hvac-community-events' ) + ), + 'welcome_on_registration' => array( + 'name' => __( 'Welcome Email on Registration', 'hvac-community-events' ), + 'trigger_type' => 'on_registration', + 'trigger_value' => 0, + 'trigger_unit' => 'minutes', + 'template_category' => 'pre_event', + 'target_audience' => 'all_attendees', + 'description' => __( 'Send welcome email immediately when someone registers', 'hvac-community-events' ) + ), + 'post_event_followup' => array( + 'name' => __( 'Post-Event Follow-up', 'hvac-community-events' ), + 'trigger_type' => 'after_event', + 'trigger_value' => 2, + 'trigger_unit' => 'days', + 'template_category' => 'post_event', + 'target_audience' => 'all_attendees', + 'description' => __( 'Send follow-up email 2 days after event', 'hvac-community-events' ) + ), + 'certificate_notification' => array( + 'name' => __( 'Certificate Ready Notification', 'hvac-community-events' ), + 'trigger_type' => 'after_event', + 'trigger_value' => 3, + 'trigger_unit' => 'days', + 'template_category' => 'certificate', + 'target_audience' => 'confirmed_attendees', + 'description' => __( 'Notify attendees when certificates are ready', 'hvac-community-events' ) + ) + ); + } +} + +// Initialize the scheduler +function hvac_communication_scheduler() { + return HVAC_Communication_Scheduler::instance(); +} + +// Initialize after plugins loaded +add_action( 'plugins_loaded', 'hvac_communication_scheduler' ); \ No newline at end of file diff --git a/includes/communication/class-communication-templates.php b/includes/communication/class-communication-templates.php new file mode 100644 index 00000000..8aecb3a1 --- /dev/null +++ b/includes/communication/class-communication-templates.php @@ -0,0 +1,518 @@ + 'Attendee Name', + '{event_title}' => 'Event Title', + '{event_date}' => 'Event Date', + '{event_time}' => 'Event Time', + '{event_location}' => 'Event Location', + '{trainer_name}' => 'Trainer Name', + '{business_name}' => 'Business Name', + '{trainer_email}' => 'Trainer Email', + '{trainer_phone}' => 'Trainer Phone', + '{current_date}' => 'Current Date', + '{website_name}' => 'Website Name', + '{website_url}' => 'Website URL' + ); + + /** + * Default template categories + */ + const DEFAULT_CATEGORIES = array( + 'pre_event' => 'Pre-Event Communications', + 'event_reminder' => 'Event Reminders', + 'post_event' => 'Post-Event Follow-up', + 'certificate' => 'Certificate Information', + 'general' => 'General Communications' + ); + + /** + * Constructor + */ + public function __construct() { + add_action( 'init', array( $this, 'register_post_type' ) ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); + add_action( 'wp_ajax_hvac_save_template', array( $this, 'ajax_save_template' ) ); + add_action( 'wp_ajax_hvac_load_template', array( $this, 'ajax_load_template' ) ); + add_action( 'wp_ajax_hvac_delete_template', array( $this, 'ajax_delete_template' ) ); + add_action( 'wp_ajax_hvac_get_templates', array( $this, 'ajax_get_templates' ) ); + + // Debug logging + if ( class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::info( 'HVAC_Communication_Templates class instantiated', 'Templates' ); + } + } + + /** + * Register the email template custom post type + */ + public function register_post_type() { + $args = array( + 'label' => __( 'Email Templates', 'hvac-community-events' ), + 'labels' => array( + 'name' => __( 'Email Templates', 'hvac-community-events' ), + 'singular_name' => __( 'Email Template', 'hvac-community-events' ), + ), + 'public' => false, + 'publicly_queryable' => false, + 'show_ui' => false, + 'show_in_menu' => false, + 'show_in_rest' => true, // Enable REST API access + 'rest_base' => 'hvac_email_templates', + 'rest_controller_class' => 'WP_REST_Posts_Controller', + 'supports' => array( 'title', 'editor', 'author' ), + 'capability_type' => 'post', + 'capabilities' => array( + 'create_posts' => 'edit_posts', + 'delete_posts' => 'delete_posts', + 'delete_others_posts' => 'delete_others_posts', + 'delete_private_posts' => 'delete_private_posts', + 'delete_published_posts' => 'delete_published_posts', + 'edit_posts' => 'edit_posts', + 'edit_others_posts' => 'edit_others_posts', + 'edit_private_posts' => 'edit_private_posts', + 'edit_published_posts' => 'edit_published_posts', + 'publish_posts' => 'publish_posts', + 'read_private_posts' => 'read_private_posts', + ), + 'hierarchical' => false, + 'has_archive' => false, + 'rewrite' => false, + ); + + register_post_type( self::POST_TYPE, $args ); + } + + /** + * Enqueue scripts for template management + */ + public function enqueue_scripts() { + global $post; + + // Check if we're on a relevant page + $should_enqueue = false; + + if ( is_a( $post, 'WP_Post' ) ) { + // Check for shortcodes + if ( has_shortcode( $post->post_content, 'hvac_email_attendees' ) || + has_shortcode( $post->post_content, 'hvac_communication_templates' ) ) { + $should_enqueue = true; + } + + // Also check by page slug + if ( $post->post_name === 'communication-templates' || $post->post_name === 'email-attendees' ) { + $should_enqueue = true; + } + } + + // Also check if we're on specific pages by is_page + if ( is_page( 'communication-templates' ) || is_page( 'email-attendees' ) ) { + $should_enqueue = true; + } + + if ( $should_enqueue ) { + // Debug logging + if ( class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::info( 'Enqueuing template scripts and styles', 'Templates' ); + } + + wp_enqueue_script( + 'hvac-communication-templates', + HVAC_PLUGIN_URL . 'assets/js/communication-templates.js', + array( 'jquery' ), + HVAC_PLUGIN_VERSION, + true + ); + + wp_localize_script( 'hvac-communication-templates', 'hvacTemplates', array( + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'hvac_templates_nonce' ), + 'placeholders' => self::PLACEHOLDERS, + 'categories' => self::DEFAULT_CATEGORIES, + 'strings' => array( + 'saveTemplate' => __( 'Save Template', 'hvac-community-events' ), + 'templateSaved' => __( 'Template saved successfully', 'hvac-community-events' ), + 'templateDeleted' => __( 'Template deleted successfully', 'hvac-community-events' ), + 'confirmDelete' => __( 'Are you sure you want to delete this template?', 'hvac-community-events' ), + 'error' => __( 'An error occurred. Please try again.', 'hvac-community-events' ), + 'templateName' => __( 'Template Name', 'hvac-community-events' ), + 'selectCategory' => __( 'Select Category', 'hvac-community-events' ), + 'insertPlaceholder' => __( 'Insert Placeholder', 'hvac-community-events' ), + ) + ) ); + + wp_enqueue_style( + 'hvac-communication-templates', + HVAC_PLUGIN_URL . 'assets/css/communication-templates.css', + array(), + HVAC_PLUGIN_VERSION + ); + } + } + + /** + * Get templates for a specific user + * + * @param int $user_id User ID (defaults to current user) + * @param string $category Optional category filter + * @return array Array of templates + */ + public function get_user_templates( $user_id = 0, $category = '' ) { + if ( empty( $user_id ) ) { + $user_id = get_current_user_id(); + } + + $args = array( + 'post_type' => self::POST_TYPE, + 'author' => $user_id, + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'orderby' => 'title', + 'order' => 'ASC', + ); + + if ( ! empty( $category ) ) { + $args['meta_query'] = array( + array( + 'key' => '_hvac_template_category', + 'value' => $category, + 'compare' => '=' + ) + ); + } + + $templates = get_posts( $args ); + $formatted_templates = array(); + + foreach ( $templates as $template ) { + $formatted_templates[] = array( + 'id' => $template->ID, + 'title' => $template->post_title, + 'content' => $template->post_content, + 'category' => get_post_meta( $template->ID, '_hvac_template_category', true ), + 'description' => get_post_meta( $template->ID, '_hvac_template_description', true ), + 'created' => $template->post_date, + 'modified' => $template->post_modified, + ); + } + + return $formatted_templates; + } + + /** + * Save a template + * + * @param array $template_data Template data + * @return int|WP_Error Template ID on success, WP_Error on failure + */ + public function save_template( $template_data ) { + // Validate required fields + if ( empty( $template_data['title'] ) || empty( $template_data['content'] ) ) { + return new WP_Error( 'missing_data', __( 'Template title and content are required.', 'hvac-community-events' ) ); + } + + $post_data = array( + 'post_type' => self::POST_TYPE, + 'post_title' => sanitize_text_field( $template_data['title'] ), + 'post_content' => wp_kses_post( $template_data['content'] ), + 'post_status' => 'publish', + 'post_author' => get_current_user_id(), + ); + + // Update existing template if ID provided + if ( ! empty( $template_data['id'] ) ) { + $post_data['ID'] = intval( $template_data['id'] ); + + // Verify ownership + $existing_template = get_post( $post_data['ID'] ); + if ( ! $existing_template || $existing_template->post_author != get_current_user_id() ) { + return new WP_Error( 'permission_denied', __( 'You can only edit your own templates.', 'hvac-community-events' ) ); + } + } + + $template_id = wp_insert_post( $post_data ); + + if ( is_wp_error( $template_id ) ) { + return $template_id; + } + + // Save metadata + if ( ! empty( $template_data['category'] ) ) { + update_post_meta( $template_id, '_hvac_template_category', sanitize_text_field( $template_data['category'] ) ); + } + + if ( ! empty( $template_data['description'] ) ) { + update_post_meta( $template_id, '_hvac_template_description', sanitize_text_field( $template_data['description'] ) ); + } + + return $template_id; + } + + /** + * Delete a template + * + * @param int $template_id Template ID + * @return bool Success status + */ + public function delete_template( $template_id ) { + $template = get_post( $template_id ); + + if ( ! $template || $template->post_type !== self::POST_TYPE ) { + return false; + } + + // Verify ownership + if ( $template->post_author != get_current_user_id() && ! current_user_can( 'delete_others_posts' ) ) { + return false; + } + + return wp_delete_post( $template_id, true ) !== false; + } + + /** + * Process placeholders in template content + * + * @param string $content Template content + * @param array $context Context data for placeholders + * @return string Processed content + */ + public function process_placeholders( $content, $context = array() ) { + $current_user = wp_get_current_user(); + + // Default context values + $defaults = array( + 'attendee_name' => '', + 'event_title' => '', + 'event_date' => '', + 'event_time' => '', + 'event_location' => '', + 'trainer_name' => $current_user->display_name, + 'business_name' => get_user_meta( $current_user->ID, 'business_name', true ), + 'trainer_email' => $current_user->user_email, + 'trainer_phone' => get_user_meta( $current_user->ID, 'phone_number', true ), + 'current_date' => date( 'F j, Y' ), + 'website_name' => get_bloginfo( 'name' ), + 'website_url' => home_url(), + ); + + // Get trainer contact email if available + if ( in_array( 'hvac_trainer', $current_user->roles ) ) { + $contact_email = get_user_meta( $current_user->ID, 'contact_email', true ); + if ( ! empty( $contact_email ) && is_email( $contact_email ) ) { + $defaults['trainer_email'] = $contact_email; + } + } + + $context = wp_parse_args( $context, $defaults ); + + // Replace placeholders + foreach ( self::PLACEHOLDERS as $placeholder => $description ) { + $key = str_replace( array( '{', '}' ), '', $placeholder ); + if ( isset( $context[ $key ] ) ) { + $content = str_replace( $placeholder, $context[ $key ], $content ); + } + } + + return $content; + } + + /** + * Get default templates + * + * @return array Default templates + */ + public function get_default_templates() { + return array( + array( + 'title' => __( 'Event Reminder - 24 Hours', 'hvac-community-events' ), + 'category' => 'event_reminder', + 'description' => __( 'Reminder sent 24 hours before the event', 'hvac-community-events' ), + 'content' => "Hello {attendee_name},\n\nThis is a friendly reminder that you're registered for {event_title} tomorrow at {event_time}.\n\nEvent Details:\n- Date: {event_date}\n- Time: {event_time}\n- Location: {event_location}\n\nPlease bring a valid ID and any materials mentioned in your registration confirmation.\n\nIf you have any questions, please don't hesitate to contact me.\n\nBest regards,\n{trainer_name}\n{business_name}\n{trainer_email}\n{trainer_phone}" + ), + array( + 'title' => __( 'Welcome & Pre-Event Information', 'hvac-community-events' ), + 'category' => 'pre_event', + 'description' => __( 'Welcome message with event preparation information', 'hvac-community-events' ), + 'content' => "Welcome {attendee_name}!\n\nThank you for registering for {event_title}. I'm excited to have you join us on {event_date} at {event_time}.\n\nTo help you prepare for the training:\n\n1. Please arrive 15 minutes early for check-in\n2. Bring a valid photo ID\n3. Dress comfortably and wear closed-toe shoes\n4. Bring a notebook and pen for taking notes\n5. Lunch will be provided\n\nIf you have any questions before the event, please feel free to reach out.\n\nLooking forward to seeing you there!\n\n{trainer_name}\n{business_name}\n{trainer_email}\n{trainer_phone}" + ), + array( + 'title' => __( 'Thank You & Certificate Information', 'hvac-community-events' ), + 'category' => 'post_event', + 'description' => __( 'Post-event thank you with certificate details', 'hvac-community-events' ), + 'content' => "Dear {attendee_name},\n\nThank you for attending {event_title} on {event_date}. It was great having you participate in the training.\n\nYour certificate of completion will be available within 3-5 business days. You can download it from your attendee profile on our website.\n\nIf you have any questions about the training content or need additional resources, please don't hesitate to contact me.\n\nThank you again for your participation, and I look forward to seeing you at future training events.\n\nBest regards,\n{trainer_name}\n{business_name}\n{trainer_email}\n{trainer_phone}" + ), + array( + 'title' => __( 'Certificate Ready for Download', 'hvac-community-events' ), + 'category' => 'certificate', + 'description' => __( 'Notification when certificate is ready', 'hvac-community-events' ), + 'content' => "Hello {attendee_name},\n\nGreat news! Your certificate of completion for {event_title} is now ready for download.\n\nTo access your certificate:\n1. Visit {website_url}\n2. Log into your attendee profile\n3. Navigate to the 'My Certificates' section\n4. Download your certificate for {event_title}\n\nYour certificate includes:\n- Official completion verification\n- Training date and hours\n- Digital security features\n- Suitable for continuing education records\n\nIf you have any trouble accessing your certificate, please contact me directly.\n\nCongratulations on completing the training!\n\n{trainer_name}\n{business_name}\n{trainer_email}\n{trainer_phone}" + ) + ); + } + + /** + * Install default templates for a user + * + * @param int $user_id User ID + */ + public function install_default_templates( $user_id ) { + $defaults = $this->get_default_templates(); + + foreach ( $defaults as $template ) { + $template_data = array( + 'title' => $template['title'], + 'content' => $template['content'], + 'category' => $template['category'], + 'description' => $template['description'], + ); + + // Temporarily switch to the target user + $current_user_id = get_current_user_id(); + wp_set_current_user( $user_id ); + + $this->save_template( $template_data ); + + // Switch back to original user + wp_set_current_user( $current_user_id ); + } + } + + /** + * AJAX handler for saving templates + */ + public function ajax_save_template() { + check_ajax_referer( 'hvac_templates_nonce', 'nonce' ); + + if ( ! is_user_logged_in() ) { + wp_send_json_error( array( 'message' => __( 'You must be logged in to save templates.', 'hvac-community-events' ) ) ); + } + + $template_data = array( + 'id' => isset( $_POST['template_id'] ) ? intval( $_POST['template_id'] ) : 0, + 'title' => isset( $_POST['title'] ) ? sanitize_text_field( $_POST['title'] ) : '', + 'content' => isset( $_POST['content'] ) ? wp_kses_post( $_POST['content'] ) : '', + 'category' => isset( $_POST['category'] ) ? sanitize_text_field( $_POST['category'] ) : '', + 'description' => isset( $_POST['description'] ) ? sanitize_text_field( $_POST['description'] ) : '', + ); + + $result = $this->save_template( $template_data ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ) ); + } + + wp_send_json_success( array( + 'template_id' => $result, + 'message' => __( 'Template saved successfully.', 'hvac-community-events' ) + ) ); + } + + /** + * AJAX handler for loading templates + */ + public function ajax_load_template() { + check_ajax_referer( 'hvac_templates_nonce', 'nonce' ); + + if ( ! is_user_logged_in() ) { + wp_send_json_error( array( 'message' => __( 'You must be logged in to load templates.', 'hvac-community-events' ) ) ); + } + + $template_id = isset( $_POST['template_id'] ) ? intval( $_POST['template_id'] ) : 0; + + if ( empty( $template_id ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid template ID.', 'hvac-community-events' ) ) ); + } + + $template = get_post( $template_id ); + + if ( ! $template || $template->post_type !== self::POST_TYPE ) { + wp_send_json_error( array( 'message' => __( 'Template not found.', 'hvac-community-events' ) ) ); + } + + // Verify ownership or admin access + if ( $template->post_author != get_current_user_id() && ! current_user_can( 'edit_others_posts' ) ) { + wp_send_json_error( array( 'message' => __( 'You can only load your own templates.', 'hvac-community-events' ) ) ); + } + + wp_send_json_success( array( + 'id' => $template->ID, + 'title' => $template->post_title, + 'content' => $template->post_content, + 'category' => get_post_meta( $template->ID, '_hvac_template_category', true ), + 'description' => get_post_meta( $template->ID, '_hvac_template_description', true ), + ) ); + } + + /** + * AJAX handler for deleting templates + */ + public function ajax_delete_template() { + check_ajax_referer( 'hvac_templates_nonce', 'nonce' ); + + if ( ! is_user_logged_in() ) { + wp_send_json_error( array( 'message' => __( 'You must be logged in to delete templates.', 'hvac-community-events' ) ) ); + } + + $template_id = isset( $_POST['template_id'] ) ? intval( $_POST['template_id'] ) : 0; + + if ( empty( $template_id ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid template ID.', 'hvac-community-events' ) ) ); + } + + $result = $this->delete_template( $template_id ); + + if ( ! $result ) { + wp_send_json_error( array( 'message' => __( 'Failed to delete template.', 'hvac-community-events' ) ) ); + } + + wp_send_json_success( array( 'message' => __( 'Template deleted successfully.', 'hvac-community-events' ) ) ); + } + + /** + * AJAX handler for getting templates list + */ + public function ajax_get_templates() { + check_ajax_referer( 'hvac_templates_nonce', 'nonce' ); + + if ( ! is_user_logged_in() ) { + wp_send_json_error( array( 'message' => __( 'You must be logged in to view templates.', 'hvac-community-events' ) ) ); + } + + $category = isset( $_POST['category'] ) ? sanitize_text_field( $_POST['category'] ) : ''; + $templates = $this->get_user_templates( get_current_user_id(), $category ); + + wp_send_json_success( array( 'templates' => $templates ) ); + } +} + +// Initialize the class +new HVAC_Communication_Templates(); \ No newline at end of file diff --git a/includes/communication/class-communication-trigger-engine.php b/includes/communication/class-communication-trigger-engine.php new file mode 100644 index 00000000..32054129 --- /dev/null +++ b/includes/communication/class-communication-trigger-engine.php @@ -0,0 +1,519 @@ +get_unit_multiplier( $trigger_unit ); + if ( ! $seconds_multiplier ) { + return null; + } + + $offset_seconds = $trigger_value * $seconds_multiplier; + + switch ( $schedule['trigger_type'] ) { + case 'before_event': + $trigger_timestamp = $event_timestamp - $offset_seconds; + break; + case 'after_event': + // Use event end date if available, otherwise start date + $event_end_date = get_post_meta( $schedule['event_id'], '_EventEndDate', true ); + $end_timestamp = $event_end_date ? strtotime( $event_end_date ) : $event_timestamp; + $trigger_timestamp = $end_timestamp + $offset_seconds; + break; + case 'on_registration': + // Immediate trigger - return current time + return current_time( 'mysql' ); + case 'custom_date': + // Custom date should be provided in schedule data + return isset( $schedule['custom_date'] ) ? $schedule['custom_date'] : null; + default: + return null; + } + + // Ensure trigger time is in the future + if ( $trigger_timestamp <= time() ) { + return null; + } + + return date( 'Y-m-d H:i:s', $trigger_timestamp ); + } + + /** + * Get unit multiplier for converting to seconds + * + * @param string $unit Time unit + * @return int|false Multiplier or false if invalid + */ + private function get_unit_multiplier( $unit ) { + $multipliers = array( + 'minutes' => MINUTE_IN_SECONDS, + 'hours' => HOUR_IN_SECONDS, + 'days' => DAY_IN_SECONDS, + 'weeks' => WEEK_IN_SECONDS + ); + + return isset( $multipliers[$unit] ) ? $multipliers[$unit] : false; + } + + /** + * Get recipients for a schedule based on target audience settings + * + * @param array $schedule Schedule configuration + * @return array Array of recipient data + */ + public function get_schedule_recipients( $schedule ) { + $recipients = array(); + + switch ( $schedule['target_audience'] ) { + case 'all_attendees': + $recipients = $this->get_all_event_attendees( $schedule['event_id'] ); + break; + case 'confirmed_attendees': + $recipients = $this->get_confirmed_attendees( $schedule['event_id'] ); + break; + case 'pending_attendees': + $recipients = $this->get_pending_attendees( $schedule['event_id'] ); + break; + case 'custom_list': + $recipients = $this->parse_custom_recipient_list( $schedule['custom_recipient_list'] ); + break; + } + + // Apply additional conditions if specified + if ( ! empty( $schedule['conditions'] ) ) { + $recipients = $this->apply_recipient_conditions( $recipients, $schedule['conditions'] ); + } + + return $recipients; + } + + /** + * Get all attendees for an event + * + * @param int $event_id Event ID + * @return array Array of attendee data + */ + private function get_all_event_attendees( $event_id ) { + if ( empty( $event_id ) ) { + return array(); + } + + // Use the Email Attendees Data class for consistent attendee retrieval + $email_data = new HVAC_Email_Attendees_Data( $event_id ); + $attendees = $email_data->get_attendees(); + + $recipients = array(); + foreach ( $attendees as $attendee ) { + $recipients[] = array( + 'email' => $attendee['email'], + 'name' => $attendee['name'], + 'attendee_id' => $attendee['attendee_id'], + 'ticket_name' => $attendee['ticket_name'], + 'status' => 'confirmed' // Default status + ); + } + + return $recipients; + } + + /** + * Get confirmed attendees only + * + * @param int $event_id Event ID + * @return array Array of confirmed attendee data + */ + private function get_confirmed_attendees( $event_id ) { + $all_attendees = $this->get_all_event_attendees( $event_id ); + + // For now, treat all attendees as confirmed + // This can be enhanced later based on ticket status if needed + return array_filter( $all_attendees, function( $attendee ) { + return $attendee['status'] === 'confirmed'; + }); + } + + /** + * Get pending attendees only + * + * @param int $event_id Event ID + * @return array Array of pending attendee data + */ + private function get_pending_attendees( $event_id ) { + $all_attendees = $this->get_all_event_attendees( $event_id ); + + return array_filter( $all_attendees, function( $attendee ) { + return $attendee['status'] === 'pending'; + }); + } + + /** + * Parse custom recipient list from text input + * + * @param string $recipient_list Comma or line-separated email list + * @return array Array of recipient data + */ + private function parse_custom_recipient_list( $recipient_list ) { + if ( empty( $recipient_list ) ) { + return array(); + } + + $recipients = array(); + $lines = preg_split( '/[\r\n,]+/', $recipient_list ); + + foreach ( $lines as $line ) { + $line = trim( $line ); + + if ( empty( $line ) ) { + continue; + } + + // Check if line contains both name and email + if ( preg_match( '/(.+?)\s*<(.+?)>/', $line, $matches ) ) { + $name = trim( $matches[1] ); + $email = trim( $matches[2] ); + } else { + // Just email address + $email = $line; + $name = ''; + } + + if ( is_email( $email ) ) { + $recipients[] = array( + 'email' => $email, + 'name' => $name, + 'attendee_id' => 0, + 'ticket_name' => '', + 'status' => 'custom' + ); + } + } + + return $recipients; + } + + /** + * Apply additional conditions to filter recipients + * + * @param array $recipients Current recipient list + * @param array $conditions Filter conditions + * @return array Filtered recipients + */ + private function apply_recipient_conditions( $recipients, $conditions ) { + if ( empty( $conditions ) ) { + return $recipients; + } + + foreach ( $conditions as $condition ) { + switch ( $condition['type'] ) { + case 'ticket_type': + $recipients = array_filter( $recipients, function( $recipient ) use ( $condition ) { + return $recipient['ticket_name'] === $condition['value']; + }); + break; + case 'exclude_emails': + $exclude_list = array_map( 'trim', explode( ',', $condition['value'] ) ); + $recipients = array_filter( $recipients, function( $recipient ) use ( $exclude_list ) { + return ! in_array( $recipient['email'], $exclude_list ); + }); + break; + } + } + + return $recipients; + } + + /** + * Execute communication for a schedule + * + * @param array $schedule Schedule configuration + * @param array $recipients Recipients to send to + * @return bool Success status + */ + public function execute_communication( $schedule, $recipients ) { + if ( empty( $recipients ) || empty( $schedule['template_id'] ) ) { + return false; + } + + // Get the email template + $template = get_post( $schedule['template_id'] ); + if ( ! $template || $template->post_type !== 'hvac_email_template' ) { + return false; + } + + $subject = $template->post_title; + $message = $template->post_content; + + // Get event details for placeholder replacement + $event_details = null; + if ( ! empty( $schedule['event_id'] ) ) { + $email_data = new HVAC_Email_Attendees_Data( $schedule['event_id'] ); + $event_details = $email_data->get_event_details(); + } + + $success_count = 0; + $total_count = count( $recipients ); + + foreach ( $recipients as $recipient ) { + // Replace placeholders in subject and message + $personalized_subject = $this->replace_placeholders( $subject, $recipient, $event_details ); + $personalized_message = $this->replace_placeholders( $message, $recipient, $event_details ); + + // Send email + $headers = array( + 'Content-Type: text/html; charset=UTF-8' + ); + + // Add sender information + $trainer = get_user_by( 'id', $schedule['trainer_id'] ); + if ( $trainer ) { + $from_name = $trainer->display_name; + $from_email = $trainer->user_email; + + // Check for trainer business name + $business_name = get_user_meta( $trainer->ID, 'business_name', true ); + if ( ! empty( $business_name ) ) { + $from_name = $business_name; + } + + $headers[] = 'From: ' . $from_name . ' <' . $from_email . '>'; + } + + $mail_sent = wp_mail( $recipient['email'], $personalized_subject, wpautop( $personalized_message ), $headers ); + + if ( $mail_sent ) { + $success_count++; + } + + // Log individual send attempt if logger is available + if ( class_exists( 'HVAC_Logger' ) ) { + $status = $mail_sent ? 'sent' : 'failed'; + HVAC_Logger::info( "Email {$status} to {$recipient['email']} for schedule {$schedule['schedule_id']}", 'Communication Engine' ); + } + } + + return $success_count === $total_count; + } + + /** + * Replace placeholders in email content + * + * @param string $content Email subject or content + * @param array $recipient Recipient data + * @param array|null $event_details Event details for placeholders + * @return string Content with placeholders replaced + */ + private function replace_placeholders( $content, $recipient, $event_details = null ) { + $placeholders = array( + '{attendee_name}' => $recipient['name'], + '{attendee_email}' => $recipient['email'], + '{ticket_type}' => $recipient['ticket_name'] + ); + + if ( $event_details ) { + $placeholders['{event_title}'] = $event_details['title']; + $placeholders['{event_date}'] = $event_details['start_date']; + $placeholders['{event_time}'] = $event_details['start_time']; + $placeholders['{event_start_date}'] = $event_details['start_date']; + $placeholders['{event_start_time}'] = $event_details['start_time']; + $placeholders['{event_end_date}'] = $event_details['end_date']; + $placeholders['{event_end_time}'] = $event_details['end_time']; + } + + // Add current date/time placeholders + $placeholders['{current_date}'] = date( 'F j, Y' ); + $placeholders['{current_time}'] = date( 'g:i a' ); + $placeholders['{current_year}'] = date( 'Y' ); + + return str_replace( array_keys( $placeholders ), array_values( $placeholders ), $content ); + } + + /** + * Process registration-triggered communications + * + * @param int $attendee_id Attendee ID + * @param int $event_id Event ID + */ + public function process_registration_triggers( $attendee_id, $event_id ) { + global $wpdb; + + // Get all active schedules with registration triggers for this event + $schedules_table = $wpdb->prefix . 'hvac_communication_schedules'; + + $schedules = $wpdb->get_results( $wpdb->prepare( + "SELECT * FROM {$schedules_table} + WHERE event_id = %d + AND trigger_type = 'on_registration' + AND status = 'active'", + $event_id + ), ARRAY_A ); + + foreach ( $schedules as $schedule ) { + // Get attendee details + $attendee_post = get_post( $attendee_id ); + if ( ! $attendee_post ) { + continue; + } + + $attendee_email = get_post_meta( $attendee_id, '_tribe_tickets_email', true ); + if ( empty( $attendee_email ) ) { + $attendee_email = get_post_meta( $attendee_id, '_tribe_tpp_email', true ); + } + + $attendee_name = get_post_meta( $attendee_id, '_tribe_tickets_full_name', true ); + if ( empty( $attendee_name ) ) { + $attendee_name = get_post_meta( $attendee_id, '_tribe_tpp_full_name', true ); + } + + if ( empty( $attendee_email ) || ! is_email( $attendee_email ) ) { + continue; + } + + // Create recipient array + $recipients = array( + array( + 'email' => $attendee_email, + 'name' => $attendee_name, + 'attendee_id' => $attendee_id, + 'ticket_name' => '', + 'status' => 'confirmed' + ) + ); + + // Execute communication + $this->execute_communication( $schedule, $recipients ); + + // Update schedule run tracking + $schedule_manager = new HVAC_Communication_Schedule_Manager(); + $schedule_manager->update_schedule_run_tracking( $schedule['schedule_id'] ); + } + } + + /** + * Process event date changes and update affected schedules + */ + public function process_event_date_changes() { + global $wpdb; + + // This would be called when event dates are updated + // For now, it's a placeholder for future implementation + + if ( class_exists( 'HVAC_Logger' ) ) { + HVAC_Logger::info( 'Processing event date changes', 'Communication Engine' ); + } + } + + /** + * Validate recipients against event attendees + * + * @param array $recipients Recipients to validate + * @param int $event_id Event ID + * @return array Valid recipients only + */ + public function validate_recipients( $recipients, $event_id = null ) { + if ( empty( $recipients ) ) { + return array(); + } + + $valid_recipients = array(); + + foreach ( $recipients as $recipient ) { + // Basic email validation + if ( empty( $recipient['email'] ) || ! is_email( $recipient['email'] ) ) { + continue; + } + + // If event ID provided, verify recipient is actually an attendee + if ( $event_id ) { + $all_attendees = $this->get_all_event_attendees( $event_id ); + $is_attendee = false; + + foreach ( $all_attendees as $attendee ) { + if ( $attendee['email'] === $recipient['email'] ) { + $is_attendee = true; + break; + } + } + + if ( ! $is_attendee && $recipient['status'] !== 'custom' ) { + continue; + } + } + + $valid_recipients[] = $recipient; + } + + return $valid_recipients; + } + + /** + * Get communication statistics for a schedule + * + * @param int $schedule_id Schedule ID + * @return array Statistics array + */ + public function get_schedule_statistics( $schedule_id ) { + global $wpdb; + + $logs_table = $wpdb->prefix . 'hvac_communication_logs'; + + $stats = $wpdb->get_row( $wpdb->prepare( + "SELECT + COUNT(*) as total_sends, + COUNT(CASE WHEN status = 'sent' THEN 1 END) as successful_sends, + COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed_sends, + MAX(sent_date) as last_sent + FROM {$logs_table} + WHERE schedule_id = %d", + $schedule_id + ), ARRAY_A ); + + return $stats; + } +} \ No newline at end of file diff --git a/includes/community/class-email-attendees-data.php b/includes/community/class-email-attendees-data.php new file mode 100644 index 00000000..e860f6bd --- /dev/null +++ b/includes/community/class-email-attendees-data.php @@ -0,0 +1,470 @@ +event_id = intval( $event_id ); + } + + /** + * Check if the event is valid. + * + * @return bool Whether the event exists and is valid. + */ + public function is_valid_event() { + if ( empty( $this->event_id ) ) { + return false; + } + + $event = get_post( $this->event_id ); + return ( $event && $event->post_type === 'tribe_events' ); + } + + /** + * Check if the current user can view and email attendees for this event. + * + * @return bool Whether the user can view and email attendees. + */ + public function user_can_email_attendees() { + if ( ! is_user_logged_in() ) { + return false; + } + + $event = get_post( $this->event_id ); + if ( ! $event ) { + return false; + } + + // Allow event author or admins with edit_posts capability + return ( get_current_user_id() === (int) $event->post_author || current_user_can( 'edit_posts' ) ); + } + + /** + * Get all attendees for the event. + * + * @return array Array of attendee data. + */ + public function get_attendees() { + if ( ! $this->is_valid_event() ) { + return array(); + } + + $processed_attendees = array(); + + // First try using The Events Calendar's function + if (function_exists('tribe_tickets_get_attendees')) { + $attendees = tribe_tickets_get_attendees( $this->event_id ); + + if ( ! empty( $attendees ) ) { + foreach ( $attendees as $attendee ) { + $email = isset( $attendee['holder_email'] ) ? $attendee['holder_email'] : ''; + if (empty($email) && isset($attendee['purchaser_email'])) { + $email = $attendee['purchaser_email']; + } + + $name = isset( $attendee['holder_name'] ) ? $attendee['holder_name'] : ''; + if (empty($name) && isset($attendee['purchaser_name'])) { + $name = $attendee['purchaser_name']; + } + + $ticket_name = isset( $attendee['ticket_name'] ) ? $attendee['ticket_name'] : ''; + + // Only include attendees with valid emails + if ( ! empty( $email ) && is_email( $email ) ) { + $processed_attendees[] = array( + 'name' => $name, + 'email' => $email, + 'ticket_name' => $ticket_name, + 'attendee_id' => isset( $attendee['attendee_id'] ) ? $attendee['attendee_id'] : 0, + 'order_id' => isset( $attendee['order_id'] ) ? $attendee['order_id'] : 0, + ); + } + } + } + } + + // If no attendees found or function doesn't exist, fall back to direct query + if (empty($processed_attendees)) { + $processed_attendees = $this->get_attendees_fallback(); + } + + return $processed_attendees; + } + + /** + * Fallback method to get attendees directly from the database + * + * @return array Array of attendee data + */ + private function get_attendees_fallback() { + $processed_attendees = array(); + + // Query for attendees directly from the database + $attendees_query = new WP_Query([ + 'post_type' => 'tribe_tpp_attendees', + 'posts_per_page' => -1, + 'meta_query' => [ + [ + 'key' => '_tribe_tpp_event', + 'value' => $this->event_id, + 'compare' => '=', + ], + ], + ]); + + if ($attendees_query->have_posts()) { + while ($attendees_query->have_posts()) { + $attendees_query->the_post(); + $attendee_id = get_the_ID(); + + // Get associated ticket + $ticket_id = get_post_meta($attendee_id, '_tribe_tpp_product', true); + $ticket_name = $ticket_id ? get_the_title($ticket_id) : 'General Admission'; + + // Get purchaser details + $name = get_post_meta($attendee_id, '_tribe_tickets_full_name', true); + if (empty($name)) { + $name = get_post_meta($attendee_id, '_tribe_tpp_full_name', true); + } + if (empty($name)) { + $name = get_the_title($attendee_id); + } + + $email = get_post_meta($attendee_id, '_tribe_tickets_email', true); + if (empty($email)) { + $email = get_post_meta($attendee_id, '_tribe_tpp_email', true); + } + + // Get order info + $order_id = get_post_meta($attendee_id, '_tribe_tpp_order', true); + + // Only include attendees with valid emails + if (!empty($email) && is_email($email)) { + $processed_attendees[] = array( + 'name' => $name, + 'email' => $email, + 'ticket_name' => $ticket_name, + 'attendee_id' => $attendee_id, + 'order_id' => $order_id, + ); + } + } + wp_reset_postdata(); + } + + return $processed_attendees; + } + + /** + * Get attendees filtered by ticket type. + * + * @param string $ticket_type The ticket type to filter by. + * @return array Filtered attendees. + */ + public function get_attendees_by_ticket_type( $ticket_type ) { + $attendees = $this->get_attendees(); + + if ( empty( $ticket_type ) ) { + return $attendees; + } + + return array_filter( $attendees, function( $attendee ) use ( $ticket_type ) { + return $attendee['ticket_name'] === $ticket_type; + }); + } + + /** + * Get all ticket types for the event. + * + * @return array Array of ticket types. + */ + public function get_ticket_types() { + $attendees = $this->get_attendees(); + $ticket_types = array(); + + foreach ( $attendees as $attendee ) { + if ( ! empty( $attendee['ticket_name'] ) && ! in_array( $attendee['ticket_name'], $ticket_types ) ) { + $ticket_types[] = $attendee['ticket_name']; + } + } + + return $ticket_types; + } + + /** + * Get the event details. + * + * @return array Event details. + */ + public function get_event_details() { + if ( ! $this->is_valid_event() ) { + return array(); + } + + $event = get_post( $this->event_id ); + + return array( + 'id' => $this->event_id, + 'title' => get_the_title( $event ), + 'start_date' => tribe_get_start_date( $event, false, 'F j, Y' ), + 'start_time' => tribe_get_start_date( $event, false, 'g:i a' ), + 'end_date' => tribe_get_end_date( $event, false, 'F j, Y' ), + 'end_time' => tribe_get_end_date( $event, false, 'g:i a' ), + ); + } + + /** + * Send email to attendees. + * + * @param array $recipients Array of recipient emails or attendee IDs. + * @param string $subject The email subject. + * @param string $message The email message. + * @param string $cc Optional CC email addresses. + * @return array Result with status and message. + */ + public function send_email( $recipients, $subject, $message, $cc = '' ) { + // Start debug log + $debug_log = "=== Email Sending Debug ===\n"; + + if ( empty( $recipients ) || empty( $subject ) || empty( $message ) ) { + $debug_log .= "Error: Missing required fields\n"; + if (class_exists('HVAC_Logger')) { + HVAC_Logger::error('Email sending failed: Missing required fields', 'Email System'); + } + return array( + 'success' => false, + 'message' => 'Missing required fields (recipients, subject, or message).', + ); + } + + if ( ! $this->is_valid_event() || ! $this->user_can_email_attendees() ) { + $debug_log .= "Error: Permission denied\n"; + if (class_exists('HVAC_Logger')) { + HVAC_Logger::error('Email sending failed: Permission denied', 'Email System'); + } + return array( + 'success' => false, + 'message' => 'You do not have permission to email attendees for this event.', + ); + } + + $headers = array('Content-Type: text/html; charset=UTF-8'); + $event_details = $this->get_event_details(); + $event_title = $event_details['title']; + $debug_log .= "Event: {$event_title} (ID: {$this->event_id})\n"; + + // Add CC if provided + if ( ! empty( $cc ) ) { + $cc_emails = explode( ',', $cc ); + foreach ( $cc_emails as $cc_email ) { + $cc_email = trim( $cc_email ); + if ( is_email( $cc_email ) ) { + $headers[] = 'Cc: ' . $cc_email; + $debug_log .= "Added CC: {$cc_email}\n"; + } + } + } + + // Add sender information from the logged-in trainer + $current_user = wp_get_current_user(); + + // Get trainer profile data if available + $trainer_name = $current_user->display_name; + $trainer_email = $current_user->user_email; + + // Check if user is a trainer and has profile data + if (in_array('hvac_trainer', $current_user->roles)) { + // Try to get trainer business name first + $business_name = get_user_meta($current_user->ID, 'business_name', true); + if (!empty($business_name)) { + $trainer_name = $business_name; + } + + // Try to get trainer contact email if different + $contact_email = get_user_meta($current_user->ID, 'contact_email', true); + if (!empty($contact_email) && is_email($contact_email)) { + $trainer_email = $contact_email; + } + } + + $from_name = $trainer_name; + $from_email = $trainer_email; + $headers[] = 'From: ' . $from_name . ' <' . $from_email . '>'; + $debug_log .= "From: {$from_name} <{$from_email}>\n"; + $debug_log .= "User role: " . implode(', ', $current_user->roles) . "\n"; + + // Process recipients + $all_attendees = $this->get_attendees(); + $debug_log .= "Total attendees found: " . count($all_attendees) . "\n"; + + $attendee_emails = array(); + $sent_count = 0; + $error_count = 0; + + $debug_log .= "Recipients provided: " . count($recipients) . "\n"; + + // Handle numeric IDs or email addresses + foreach ( $recipients as $recipient ) { + $debug_log .= "Processing recipient: {$recipient}\n"; + + if ( is_numeric( $recipient ) ) { + $debug_log .= "Recipient is numeric ID\n"; + // Find attendee by ID + foreach ( $all_attendees as $attendee ) { + if ( $attendee['attendee_id'] == $recipient ) { + $attendee_emails[$attendee['email']] = $attendee['name']; + $debug_log .= "Matched with attendee: {$attendee['name']} <{$attendee['email']}>\n"; + break; + } + } + } elseif ( is_email( $recipient ) ) { + $debug_log .= "Recipient is email address\n"; + // Add directly if it's an email + $attendee_name = ''; + foreach ( $all_attendees as $attendee ) { + if ( $attendee['email'] === $recipient ) { + $attendee_name = $attendee['name']; + $debug_log .= "Matched with attendee name: {$attendee_name}\n"; + break; + } + } + $attendee_emails[$recipient] = $attendee_name; + } else { + $debug_log .= "Invalid recipient format\n"; + } + } + + $debug_log .= "Recipients to email: " . count($attendee_emails) . "\n"; + + if (empty($attendee_emails)) { + $debug_log .= "No valid recipients found! Using fallback to direct send.\n"; + + // Fallback - directly use the first selected email + foreach ($recipients as $recipient) { + if (is_email($recipient)) { + $attendee_emails[$recipient] = ''; + $debug_log .= "Added direct recipient: {$recipient}\n"; + break; + } + } + } + + // Subject with event title + $email_subject = sprintf( '[%s] %s', $event_title, $subject ); + $debug_log .= "Email subject: {$email_subject}\n"; + + // Send to each recipient individually for personalization + foreach ( $attendee_emails as $email => $name ) { + $debug_log .= "Sending to: {$email}\n"; + + // Personalize message with attendee name if available + $personalized_message = $message; + if ( ! empty( $name ) ) { + $personalized_message = "Hello " . $name . ",\n\n" . $message; + $debug_log .= "Personalized with name: {$name}\n"; + } + + // Log complete mail params for debugging + $debug_log .= "Mail parameters:\n"; + $debug_log .= "To: {$email}\n"; + $debug_log .= "Subject: {$email_subject}\n"; + $debug_log .= "Headers: " . print_r($headers, true) . "\n"; + + // Note: consolidated error logging is added below + + // Add detailed logging + $debug_log .= "Headers: " . print_r($headers, true) . "\n"; + $debug_log .= "Sending mail with wp_mail()\n"; + + // Add robust error logging + add_action('wp_mail_failed', function($wp_error) use (&$debug_log) { + $debug_log .= "Mail error: " . $wp_error->get_error_message() . "\n"; + $debug_log .= "Error data: " . print_r($wp_error->get_error_data(), true) . "\n"; + if (class_exists('HVAC_Logger')) { + HVAC_Logger::error('WordPress Mail Error: ' . $wp_error->get_error_message() . ' - ' . print_r($wp_error->get_error_data(), true), 'Email System'); + } + }); + + // Try to log environment information + $debug_log .= "Mail environment:\n"; + $debug_log .= "WordPress version: " . get_bloginfo('version') . "\n"; + if (function_exists('phpversion')) { + $debug_log .= "PHP version: " . phpversion() . "\n"; + } + + // Check if WP Mail SMTP is active + $active_plugins = get_option('active_plugins', array()); + $wp_mail_smtp_active = false; + foreach ($active_plugins as $plugin) { + if (strpos($plugin, 'wp-mail-smtp') !== false) { + $wp_mail_smtp_active = true; + $debug_log .= "WP Mail SMTP plugin is active\n"; + break; + } + } + + // Send with standard wp_mail + $mail_sent = wp_mail($email, $email_subject, wpautop($personalized_message), $headers); + + $debug_log .= "wp_mail result: " . ($mail_sent ? 'Success' : 'Failed') . "\n"; + + if ( $mail_sent ) { + $sent_count++; + } else { + $error_count++; + } + } + + // Log the complete debug information + if (class_exists('HVAC_Logger')) { + HVAC_Logger::info($debug_log, 'Email System'); + } + + // Return results + if ( $error_count > 0 ) { + return array( + 'success' => $sent_count > 0, + 'message' => sprintf( + 'Email sent to %d recipients. Failed to send to %d recipients.', + $sent_count, + $error_count + ), + ); + } + + return array( + 'success' => true, + 'message' => sprintf( 'Email successfully sent to %d recipients.', $sent_count ), + ); + } +} \ No newline at end of file diff --git a/includes/community/class-email-debug.php b/includes/community/class-email-debug.php new file mode 100644 index 00000000..8fa1dcab --- /dev/null +++ b/includes/community/class-email-debug.php @@ -0,0 +1,305 @@ + +
+ +
+ + + + + display_name . " (" . $current_user->user_email . ")\n"; + $output .= "User roles: " . implode(', ', $current_user->roles) . "\n"; + + // Check additional user profile data + $output .= "\n=== USER PROFILE DATA ===\n"; + $business_name = get_user_meta($current_user->ID, 'business_name', true); + $contact_email = get_user_meta($current_user->ID, 'contact_email', true); + $phone = get_user_meta($current_user->ID, 'phone', true); + + $output .= "Business Name: " . ($business_name ? $business_name : 'Not set') . "\n"; + $output .= "Contact Email: " . ($contact_email ? $contact_email : 'Not set') . "\n"; + $output .= "Phone: " . ($phone ? $phone : 'Not set') . "\n"; + + // List all meta fields for debugging + $output .= "\nAll user meta fields:\n"; + $user_meta = get_user_meta($current_user->ID); + foreach ($user_meta as $key => $value) { + if (is_array($value) && isset($value[0])) { + $output .= "- {$key}: " . (strlen($value[0]) > 50 ? substr($value[0], 0, 50) . "..." : $value[0]) . "\n"; + } + } + } + + // Check event + $output .= "\n=== EVENT INFORMATION ===\n"; + $event = get_post($event_id); + if (!$event) { + $output .= "Event ID {$event_id} not found.\n"; + } else { + $output .= "Event ID: {$event_id}\n"; + $output .= "Event title: " . get_the_title($event) . "\n"; + $output .= "Event status: " . get_post_status($event) . "\n"; + $output .= "Event author: " . $event->post_author . "\n"; + + // Check if user can edit event + $output .= "Current user can edit event: " . (get_current_user_id() === (int)$event->post_author || current_user_can('edit_posts') ? 'Yes' : 'No') . "\n"; + } + + // Get attendees + $output .= "\n=== ATTENDEES DATA ===\n"; + require_once HVAC_PLUGIN_DIR . 'includes/community/class-email-attendees-data.php'; + $email_data = new HVAC_Email_Attendees_Data($event_id); + + // Check if event is valid + $output .= "Event is valid: " . ($email_data->is_valid_event() ? 'Yes' : 'No') . "\n"; + $output .= "User can email attendees: " . ($email_data->user_can_email_attendees() ? 'Yes' : 'No') . "\n"; + + // Get attendees + $attendees = $email_data->get_attendees(); + $output .= "Number of attendees found: " . count($attendees) . "\n\n"; + + if (!empty($attendees)) { + $output .= "Attendee details:\n"; + foreach ($attendees as $index => $attendee) { + $output .= "--- Attendee " . ($index + 1) . " ---\n"; + $output .= "Name: " . (!empty($attendee['name']) ? $attendee['name'] : 'No name') . "\n"; + $output .= "Email: " . (!empty($attendee['email']) ? $attendee['email'] : 'No email') . "\n"; + $output .= "Ticket: " . (!empty($attendee['ticket_name']) ? $attendee['ticket_name'] : 'No ticket name') . "\n"; + $output .= "Attendee ID: " . (!empty($attendee['attendee_id']) ? $attendee['attendee_id'] : 'No ID') . "\n"; + $output .= "Order ID: " . (!empty($attendee['order_id']) ? $attendee['order_id'] : 'No order ID') . "\n\n"; + } + } + + // Test direct attendee query + $output .= "\n=== DIRECT ATTENDEE QUERY ===\n"; + $query_args = [ + 'post_type' => 'tribe_tpp_attendees', + 'posts_per_page' => -1, + 'meta_query' => [ + [ + 'key' => '_tribe_tpp_event', + 'value' => $event_id, + 'compare' => '=', + ], + ], + ]; + + $attendees_query = new WP_Query($query_args); + $output .= "Direct query found posts: " . $attendees_query->found_posts . "\n"; + + if ($attendees_query->have_posts()) { + while ($attendees_query->have_posts()) { + $attendees_query->the_post(); + $attendee_id = get_the_ID(); + + $output .= "- Post ID: {$attendee_id}, Title: " . get_the_title() . "\n"; + + // Get metadata + $email = get_post_meta($attendee_id, '_tribe_tickets_email', true); + if (empty($email)) { + $email = get_post_meta($attendee_id, '_tribe_tpp_email', true); + } + + $output .= " Email: " . ($email ?: 'None') . "\n"; + + // Check other important meta + $ticket_id = get_post_meta($attendee_id, '_tribe_tpp_product', true); + $output .= " Ticket ID: " . ($ticket_id ?: 'None') . "\n"; + + if ($ticket_id) { + $output .= " Ticket Title: " . get_the_title($ticket_id) . "\n"; + } + + $output .= " Order ID: " . get_post_meta($attendee_id, '_tribe_tpp_order', true) . "\n"; + $output .= " Check-in: " . get_post_meta($attendee_id, '_tribe_tpp_checkin', true) . "\n"; + $output .= "\n"; + } + wp_reset_postdata(); + } + + // Test email functionality + $output .= "\n=== EMAIL FUNCTIONALITY ===\n"; + $output .= "WordPress mail function available: " . (function_exists('wp_mail') ? 'Yes' : 'No') . "\n"; + $output .= "PHP mail function available: " . (function_exists('mail') ? 'Yes' : 'No') . "\n"; + + // Get mail settings + $output .= "\nMail configuration:\n"; + $admin_email = get_option('admin_email'); + $output .= "Admin email: {$admin_email}\n"; + + // Check for mail plugins + $output .= "\nMail plugins:\n"; + $all_plugins = get_option('active_plugins', array()); + $mail_plugins_found = false; + + foreach ($all_plugins as $plugin) { + if (strpos($plugin, 'mail') !== false || strpos($plugin, 'smtp') !== false) { + $output .= "- {$plugin}\n"; + $mail_plugins_found = true; + } + } + + if (!$mail_plugins_found) { + $output .= "No mail plugins detected\n"; + } + + // Check WP Mail SMTP settings if installed + if (in_array('wp-mail-smtp/wp_mail_smtp.php', $all_plugins)) { + $output .= "\nWP Mail SMTP settings:\n"; + $smtp_settings = get_option('wp_mail_smtp', array()); + + if (!empty($smtp_settings)) { + // Don't show passwords, just configuration status + $output .= "Mailer: " . (isset($smtp_settings['mail']['mailer']) ? $smtp_settings['mail']['mailer'] : 'Not set') . "\n"; + $output .= "From Email: " . (isset($smtp_settings['mail']['from_email']) ? $smtp_settings['mail']['from_email'] : 'Not set') . "\n"; + $output .= "From Name: " . (isset($smtp_settings['mail']['from_name']) ? $smtp_settings['mail']['from_name'] : 'Not set') . "\n"; + $output .= "Return Path: " . (isset($smtp_settings['mail']['return_path']) ? 'Enabled' : 'Disabled') . "\n"; + + if (isset($smtp_settings['mail']['mailer'])) { + $mailer = $smtp_settings['mail']['mailer']; + $output .= "SMTP Host: " . (isset($smtp_settings[$mailer]['host']) ? 'Configured' : 'Not configured') . "\n"; + $output .= "SMTP Encryption: " . (isset($smtp_settings[$mailer]['encryption']) ? $smtp_settings[$mailer]['encryption'] : 'Not set') . "\n"; + $output .= "SMTP Auth: " . (isset($smtp_settings[$mailer]['auth']) ? 'Enabled' : 'Disabled') . "\n"; + $output .= "SMTP Port: " . (isset($smtp_settings[$mailer]['port']) ? $smtp_settings[$mailer]['port'] : 'Not set') . "\n"; + } + } else { + $output .= "WP Mail SMTP settings not found\n"; + } + } + + // Add test button that will send an actual test email + $output .= "\nEmail testing:\n"; + $output .= "If you need to send a test email, please use one of these options:\n"; + $output .= "1. Use the form on this page with a single recipient\n"; + $output .= "2. If using WP Mail SMTP, go to WP Mail SMTP settings and use their test email feature\n"; + $output .= "3. Contact your hosting provider if mail is still not working\n"; + + // Return debug output + wp_send_json_success($output); + } + + /** + * Log mail errors + */ + public static function log_mail_error($wp_error) { + $error_message = $wp_error->get_error_message(); + error_log('WordPress Mail Error: ' . $error_message); + + // Also log to our custom file if logging is enabled + if (class_exists('HVAC_Logger')) { + HVAC_Logger::error('WordPress Mail Error: ' . $error_message, 'Email System'); + } + } +} + +// Initialize the debugging class +HVAC_Email_Debug::init(); \ No newline at end of file diff --git a/includes/community/class-event-handler.php b/includes/community/class-event-handler.php new file mode 100644 index 00000000..88e50c56 --- /dev/null +++ b/includes/community/class-event-handler.php @@ -0,0 +1,62 @@ +init(); + } + return self::$instance; + } + + /** + * Initialize hooks. + */ + public function init() { + // REMOVED: Hooks for processing form submissions (admin_post_hvac_save_event) + // add_action( 'admin_post_hvac_save_event', [ $this, 'process_event_submission' ] ); + // add_action( 'admin_post_nopriv_hvac_save_event', [ $this, 'process_event_submission' ] ); // Handle non-logged-in attempts if necessary + + // REMOVED: Shortcode registration for [hvac_event_form] + // add_shortcode( 'hvac_event_form', [ $this, 'display_event_form_shortcode' ] ); + } + + // REMOVED: display_event_form_shortcode method as we will link to the default TEC CE form page. + + // REMOVED: process_event_submission method as TEC CE shortcode handles its own submission. + + // REMOVED: can_user_edit_event helper method as it's no longer used. + +} + +// Instantiate the class +HVAC_Event_Handler::get_instance(); \ No newline at end of file diff --git a/includes/community/class-event-summary-data.php b/includes/community/class-event-summary-data.php new file mode 100644 index 00000000..f53ec5ea --- /dev/null +++ b/includes/community/class-event-summary-data.php @@ -0,0 +1,408 @@ + 'tribe_tpp_attendees', + 'posts_per_page' => -1, + 'meta_query' => [ + [ + 'key' => '_tribe_tpp_event', + 'value' => $this->event_id, + 'compare' => '=', + ], + ], + ]); + + if ($attendees_query->have_posts()) { + while ($attendees_query->have_posts()) { + $attendees_query->the_post(); + $attendee_id = get_the_ID(); + + // Get associated ticket + $ticket_id = get_post_meta($attendee_id, '_tribe_tpp_product', true); + + // Get price from ticket + $price = 0; + if ($ticket_id) { + $price_meta = get_post_meta($ticket_id, '_price', true); + if (is_numeric($price_meta)) { + $price = (float)$price_meta; + } + } + + // Get order info + $order_id = get_post_meta($attendee_id, '_tribe_tpp_order', true); + + // Get purchaser details + $purchaser_name = get_post_meta($attendee_id, '_tribe_tickets_full_name', true); + if (empty($purchaser_name)) { + $purchaser_name = get_post_meta($attendee_id, '_tribe_tpp_full_name', true); + } + + $purchaser_email = get_post_meta($attendee_id, '_tribe_tickets_email', true); + if (empty($purchaser_email)) { + $purchaser_email = get_post_meta($attendee_id, '_tribe_tpp_email', true); + } + + // Check check-in status + $checked_in = false; + $check_in = get_post_meta($attendee_id, '_tribe_tpp_checkin', true); + if (!empty($check_in)) { + $checked_in = true; + } else { + $check_in = get_post_meta($attendee_id, 'check_in', true); + if (!empty($check_in)) { + $checked_in = true; + } + } + + // Check certificate status + $certificate_status = 'Not Generated'; + if ($certificate_manager) { + $certificate = $certificate_manager->get_certificate_by_attendee($this->event_id, $attendee_id); + if ($certificate) { + if ($certificate->revoked) { + $certificate_status = 'Revoked'; + } else { + $certificate_status = $certificate->email_sent ? 'Sent' : 'Generated'; + } + } + } + + $transactions[] = [ + 'attendee_id' => $attendee_id, + 'order_id' => $order_id, + 'ticket_type_id' => $ticket_id, + 'ticket_type_name'=> $ticket_id ? get_the_title($ticket_id) : 'N/A', + 'purchaser_name' => $purchaser_name, + 'purchaser_email' => $purchaser_email, + 'security_code' => get_post_meta($attendee_id, '_tribe_tpp_security_code', true), + 'checked_in' => $checked_in, + 'price' => $price, + 'certificate_status' => $certificate_status, + ]; + } + wp_reset_postdata(); + } + } + + /** + * The ID of the event post. + * + * @var int|null + */ + private $event_id = null; + + /** + * The event post object. + * + * @var WP_Post|null + */ + private $event_post = null; + + /** + * Constructor. + * + * @param int $event_id The ID of the event to retrieve data for. + */ + public function __construct( $event_id ) { + $this->event_id = absint( $event_id ); + if ( $this->event_id > 0 ) { + $this->event_post = get_post( $this->event_id ); + // Ensure it's an event post type (adjust post type if needed) + if ( ! $this->event_post || get_post_type( $this->event_post ) !== Tribe__Events__Main::POSTTYPE ) { + $this->event_id = null; + $this->event_post = null; + } + } + } + + /** + * Check if the event is valid. + * + * @return bool True if the event ID is valid and the post exists, false otherwise. + */ + public function is_valid_event() { + // First check if the event post exists + if (is_null($this->event_post)) { + return false; + } + + // Additional validation could be added here + + return true; + } + + /** + * Check if the current user has permission to view this event. + * + * @return bool True if the user has permission, false otherwise. + */ + public function user_can_view_event() { + // User must be logged in + if (!is_user_logged_in()) { + return false; + } + + // Event must be valid + if (!$this->is_valid_event()) { + return false; + } + + // User must be the event author or have edit_posts capability + $current_user_id = get_current_user_id(); + return ($this->event_post->post_author == $current_user_id || current_user_can('edit_posts')); + } + + /** + * Get basic event details. + * + * @return array|null An array of event details or null if the event is invalid. + */ + public function get_event_details() { + if ( ! $this->is_valid_event() ) { + return null; + } + + $details = [ + 'id' => $this->event_id, + 'title' => get_the_title( $this->event_id ), + 'description' => apply_filters( 'the_content', get_post_field( 'post_content', $this->event_id ) ), + 'excerpt' => get_the_excerpt( $this->event_id ), + 'permalink' => get_permalink( $this->event_id ), + 'start_date' => null, + 'end_date' => null, + 'cost' => null, + 'is_all_day' => false, + 'is_recurring'=> false, + 'timezone' => null, + ]; + + // Use TEC functions if available + if ( function_exists( 'tribe_get_start_date' ) ) { + $details['start_date'] = tribe_get_start_date( $this->event_id, true, 'Y-m-d H:i:s' ); // Get raw date/time + } + if ( function_exists( 'tribe_get_end_date' ) ) { + $details['end_date'] = tribe_get_end_date( $this->event_id, true, 'Y-m-d H:i:s' ); // Get raw date/time + } + if ( function_exists( 'tribe_get_cost' ) ) { + $details['cost'] = tribe_get_cost( $this->event_id, true ); + } + if ( function_exists( 'tribe_event_is_all_day' ) ) { + $details['is_all_day'] = tribe_event_is_all_day( $this->event_id ); + } + if ( function_exists( 'tribe_is_recurring_event' ) ) { + $details['is_recurring'] = tribe_is_recurring_event( $this->event_id ); + } + if ( function_exists( 'tribe_get_timezone' ) ) { + $details['timezone'] = tribe_get_timezone( $this->event_id ); + } + + return $details; + } + + /** + * Get event venue details. + * + * @return array|null An array of venue details or null if the event is invalid or has no venue. + */ + public function get_event_venue_details() { + if ( ! $this->is_valid_event() ) { + return null; + } + + $venue_details = null; + $venue_id = null; + + if ( function_exists( 'tribe_get_venue_id' ) ) { + $venue_id = tribe_get_venue_id( $this->event_id ); + } + + if ( $venue_id && function_exists( 'tribe_get_venue_details' ) ) { + // tribe_get_venue_details is deprecated, use individual functions + $venue_details = [ + 'id' => $venue_id, + 'name' => function_exists('tribe_get_venue') ? tribe_get_venue( $venue_id ) : get_the_title( $venue_id ), + 'address' => function_exists('tribe_get_full_address') ? tribe_get_full_address( $venue_id ) : null, + 'street' => function_exists('tribe_get_address') ? tribe_get_address( $venue_id ) : null, + 'city' => function_exists('tribe_get_city') ? tribe_get_city( $venue_id ) : null, + 'stateprovince' => function_exists('tribe_get_stateprovince') ? tribe_get_stateprovince( $venue_id ) : null, // Use stateprovince for consistency + 'state' => function_exists('tribe_get_state') ? tribe_get_state( $venue_id ) : null, + 'province' => function_exists('tribe_get_province') ? tribe_get_province( $venue_id ) : null, + 'zip' => function_exists('tribe_get_zip') ? tribe_get_zip( $venue_id ) : null, + 'country' => function_exists('tribe_get_country') ? tribe_get_country( $venue_id ) : null, + 'phone' => function_exists('tribe_get_phone') ? tribe_get_phone( $venue_id ) : null, + 'website' => function_exists('tribe_get_venue_website_link') ? tribe_get_venue_website_link( $venue_id, false ) : null, // Get URL only + 'map_link' => function_exists('tribe_get_map_link') ? tribe_get_map_link( $venue_id ) : null, + 'directions_link' => function_exists('tribe_get_directions_link') ? tribe_get_directions_link( $venue_id ) : null, + ]; + } + + return $venue_details; + } + + /** + * Get event organizer details. + * + * @return array|null An array of organizer details or null if the event is invalid or has no organizer. + */ + public function get_event_organizer_details() { + if ( ! $this->is_valid_event() ) { + return null; + } + + $organizer_details = null; + $organizer_ids = []; + + if ( function_exists( 'tribe_get_organizer_ids' ) ) { + $organizer_ids = tribe_get_organizer_ids( $this->event_id ); + } + + // Get details for the first organizer found + if ( ! empty( $organizer_ids ) && is_array( $organizer_ids ) ) { + $organizer_id = $organizer_ids[0]; + + if ( $organizer_id > 0 ) { + $organizer_details = [ + 'id' => $organizer_id, + 'name' => function_exists('tribe_get_organizer') ? tribe_get_organizer( $organizer_id ) : get_the_title( $organizer_id ), + 'phone' => function_exists('tribe_get_organizer_phone') ? tribe_get_organizer_phone( $organizer_id ) : null, + 'website' => function_exists('tribe_get_organizer_website_link') ? tribe_get_organizer_website_link( $organizer_id, false ) : null, // Get URL only + 'email' => function_exists('tribe_get_organizer_email') ? tribe_get_organizer_email( $organizer_id ) : null, + 'permalink' => function_exists('tribe_get_event_link') ? tribe_get_event_link( $organizer_id, false, false ) : get_permalink( $organizer_id ), // Link to organizer post + ]; + } + } + + return $organizer_details; + } + + /** + * Get transaction data associated with the event. + * Requires Event Tickets / Event Tickets Plus. + * + * @return array An array of transaction data (e.g., orders, attendees). Empty array if none or invalid event. + */ + public function get_event_transactions() { + if ( ! $this->is_valid_event() ) { + return []; + } + + $transactions = []; + + // Load certificate manager if it exists + $certificate_manager = null; + if (class_exists('HVAC_Certificate_Manager')) { + require_once HVAC_PLUGIN_DIR . 'includes/certificates/class-certificate-manager.php'; + $certificate_manager = HVAC_Certificate_Manager::instance(); + } + + // Check if Event Tickets is active and the necessary class/method exists + if ( class_exists( 'Tribe__Tickets__Tickets_Handler' ) && method_exists( Tribe__Tickets__Tickets_Handler::instance(), 'get_attendees_by_id' ) ) { + $attendees = Tribe__Tickets__Tickets_Handler::instance()->get_attendees_by_id( $this->event_id ); + + if ( is_array( $attendees ) ) { + foreach ( $attendees as $attendee ) { + // Extract relevant data - structure might vary based on ticket provider (Woo, EDD, RSVP, Tribe) + $order_id = isset( $attendee['order_id'] ) ? $attendee['order_id'] : null; + $ticket_type_id = isset( $attendee['product_id'] ) ? $attendee['product_id'] : null; // product_id often holds ticket type ID + $attendee_id = isset( $attendee['attendee_id'] ) ? $attendee['attendee_id'] : null; // Unique ID for the attendee record + + // Get purchaser info (might be stored differently depending on provider) + $purchaser_name = isset( $attendee['holder_name'] ) ? $attendee['holder_name'] : null; + $purchaser_email = isset( $attendee['holder_email'] ) ? $attendee['holder_email'] : null; + if ( empty( $purchaser_name ) && isset( $attendee['purchaser_name'] ) ) { + $purchaser_name = $attendee['purchaser_name']; + } + if ( empty( $purchaser_email ) && isset( $attendee['purchaser_email'] ) ) { + $purchaser_email = $attendee['purchaser_email']; + } + + // Get price if available (might vary based on provider) + $price = 0; + if (isset($attendee['price']) && is_numeric($attendee['price'])) { + $price = (float) $attendee['price']; + } elseif (isset($attendee['price_paid']) && is_numeric($attendee['price_paid'])) { + $price = (float) $attendee['price_paid']; + } + + // Check if a certificate exists for this attendee + $certificate_status = 'Not Generated'; + if ($certificate_manager) { + $certificate = $certificate_manager->get_certificate_by_attendee($this->event_id, $attendee_id); + if ($certificate) { + if ($certificate->revoked) { + $certificate_status = 'Revoked'; + } else { + $certificate_status = $certificate->email_sent ? 'Sent' : 'Generated'; + } + } + } + + // Check attendance status from multiple possible fields + $checked_in = false; + if (isset($attendee['check_in']) && $attendee['check_in']) { + $checked_in = true; + } elseif (isset($attendee['checked_in']) && $attendee['checked_in']) { + $checked_in = true; + } elseif (isset($attendee['_tribe_tpp_checkin']) && $attendee['_tribe_tpp_checkin']) { + $checked_in = true; + } elseif (isset($attendee['meta']) && is_array($attendee['meta'])) { + if (isset($attendee['meta']['_tribe_tpp_checkin']) && $attendee['meta']['_tribe_tpp_checkin']) { + $checked_in = true; + } + } + + $transactions[] = [ + 'attendee_id' => $attendee_id, + 'order_id' => $order_id, + 'ticket_type_id' => $ticket_type_id, + 'ticket_type_name'=> $ticket_type_id ? get_the_title( $ticket_type_id ) : 'N/A', + 'purchaser_name' => $purchaser_name, + 'purchaser_email' => $purchaser_email, + 'security_code' => isset( $attendee['security_code'] ) ? $attendee['security_code'] : null, + 'checked_in' => $checked_in, + 'price' => $price, + 'certificate_status' => $certificate_status, + ]; + } + } + } else { + // Fallback if Event Tickets Handler is not available - use direct queries + $this->get_event_transactions_fallback($transactions, $certificate_manager); + } + + // If transactions were found, update event meta for dashboard stats + if (!empty($transactions)) { + $total_sold = count($transactions); + $total_revenue = 0; + + foreach ($transactions as $transaction) { + $total_revenue += $transaction['price']; + } + + // Update the meta for future dashboard reference + update_post_meta($this->event_id, '_tribe_tickets_sold', $total_sold); + update_post_meta($this->event_id, '_tribe_revenue_total', $total_revenue); + } + + return $transactions; + } +} \ No newline at end of file diff --git a/includes/community/class-login-handler.php b/includes/community/class-login-handler.php new file mode 100644 index 00000000..b7a46b46 --- /dev/null +++ b/includes/community/class-login-handler.php @@ -0,0 +1,265 @@ +' . esc_html__( 'Invalid username or password.', 'hvac-community-events' ) . ''; + } + + // Define variables needed by the template (if any) + // $caption = __( 'Please log in to access the trainer area.', 'hvac-community-events' ); + + // Include the custom login form template. + // Use a helper function to locate the template, allowing theme overrides. + $template_path = \HVAC_PLUGIN_DIR . 'templates/community/login-form.php'; // Use HVAC_PLUGIN_DIR constant + if ( file_exists( $template_path ) ) { + include $template_path; + } else { + // Fallback or error message if template is missing + echo '

Error: Login form template not found.

'; + } + + // Return the buffered content. + return ob_get_clean(); + } + + /** + * Enqueues scripts and styles for the login page. + */ + public function enqueue_scripts() { + global $post; + + // Only enqueue if the shortcode is present on the current page. + if ( is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, 'hvac_community_login' ) ) { + // Enqueue common HVAC styles + wp_enqueue_style( + 'hvac-common-style', + \HVAC_PLUGIN_URL . 'assets/css/hvac-common.css', + array(), + \HVAC_PLUGIN_VERSION + ); + + // Enqueue harmonized framework + wp_enqueue_style( + 'hvac-harmonized-framework', + \HVAC_PLUGIN_URL . 'assets/css/hvac-harmonized.css', + array('hvac-common-style'), + \HVAC_PLUGIN_VERSION + ); + + // Enqueue base login CSS + wp_enqueue_style( + 'hvac-community-login', + \HVAC_PLUGIN_URL . 'assets/css/community-login.css', + array('hvac-harmonized-framework'), + \HVAC_PLUGIN_VERSION + ); + + // Enqueue enhanced CSS + wp_enqueue_style( + 'hvac-community-login-enhanced', + \HVAC_PLUGIN_URL . 'assets/css/community-login-enhanced.css', + array('hvac-community-login'), + \HVAC_PLUGIN_VERSION + ); + + // Enqueue jQuery (dependency for our JavaScript) + wp_enqueue_script('jquery'); + + // Enqueue login JavaScript + wp_enqueue_script( + 'hvac-community-login-js', + \HVAC_PLUGIN_URL . 'assets/js/community-login.js', + array('jquery'), + \HVAC_PLUGIN_VERSION, + true + ); + + // Localize script with translatable strings + wp_localize_script('hvac-community-login-js', 'hvacLogin', array( + 'showPassword' => __('Show password', 'hvac-community-events'), + 'hidePassword' => __('Hide password', 'hvac-community-events'), + 'usernameRequired' => __('Username or email is required.', 'hvac-community-events'), + 'passwordRequired' => __('Password is required.', 'hvac-community-events'), + 'loggingIn' => __('Logging in...', 'hvac-community-events'), + 'logIn' => __('Log In', 'hvac-community-events'), + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('hvac_login_nonce') + )); + } + } + + /** + * Handles custom authentication logic (if needed). + * Placeholder for Task 2.2. + * + * @param string $username Username or email address. + * @param string $password Password. + */ + public function handle_authentication( &$username, &$password ) { + // Custom validation or checks can go here. + // For now, rely on default WordPress authentication. + } + + /** + * Handles redirecting the user back to the custom login page on authentication failure. + * + * Hooked to 'wp_login_failed'. + */ + public function handle_login_failure($username) { + // Check if the request originated from our custom login page + // We check both the referrer and the hidden field + $referrer = wp_get_referer(); + $is_custom_login = isset($_POST['hvac_custom_login']) && $_POST['hvac_custom_login'] === '1'; + $login_page_slug = 'training-login'; + + if ($is_custom_login || ($referrer && strpos($referrer, $login_page_slug) !== false)) { + $login_page_url = home_url('/' . $login_page_slug . '/'); + + // Preserve redirect_to parameter if it exists + $redirect_to = isset($_POST['redirect_to']) ? $_POST['redirect_to'] : ''; + $args = array('login' => 'failed'); + if (!empty($redirect_to)) { + $args['redirect_to'] = $redirect_to; + } + + // Redirect back to the custom login page with a failure flag + wp_safe_redirect(add_query_arg($args, $login_page_url)); + exit; + } + // If not from our custom login page, let WordPress handle normally + } + + // REMOVED: Unnecessary redirect_on_login_failure method. + // WordPress handles redirecting back to the referring page (our custom login page) + // on authentication failure automatically when using wp_login_form(). + // The 'login_redirect' filter handles the success case. + + /** + * Custom redirect logic after successful login. + * Placeholder for Task 2.5. + * Filters the login redirect URL based on user role. + * + * @param string $redirect_to The redirect destination URL. + * @param string $requested_redirect_to The requested redirect destination URL (if provided). + * @param WP_User|WP_Error $user WP_User object if login successful, WP_Error object otherwise. + * @return string Redirect URL. + */ + public function custom_login_redirect( $redirect_to, $requested_redirect_to, $user ) { + // Check if login was successful and user is not an error object + if ( $user && ! is_wp_error( $user ) ) { + // Check if the user has Master Trainer capabilities - redirect to Master Dashboard first + if ( user_can( $user, 'view_master_dashboard' ) || user_can( $user, 'view_all_trainer_data' ) ) { + // Redirect Master Trainers to the Master Dashboard + $master_dashboard_url = home_url( '/master-trainer/dashboard/' ); + return $master_dashboard_url; + } + // Check if the user has the 'hvac_trainer' role + elseif ( in_array( 'hvac_trainer', (array) $user->roles ) ) { + // Redirect regular HVAC trainers to their dashboard + // Updated to new hierarchical URL structure + $dashboard_url = home_url( '/trainer/dashboard/' ); + return $dashboard_url; + } else { + // For other roles (like admin), redirect to the standard WP admin dashboard. + // If $requested_redirect_to is set (e.g., trying to access a specific admin page), respect it. + return $requested_redirect_to ? $requested_redirect_to : admin_url(); + } + } + + // If login failed ($user is WP_Error), return the default $redirect_to. + // Our redirect_on_login_failure should ideally catch this first, but this is a fallback. + return $redirect_to; + } + + /** + * Redirects logged-in users away from the custom login page. + * Hooked to 'template_redirect'. + */ + public function redirect_logged_in_user() { + // Check if we are on the custom login page (adjust slug if needed) + if ( is_page( 'training-login' ) && is_user_logged_in() ) { + // Get current user + $user = wp_get_current_user(); + + // Redirect based on user role/capabilities - prioritize Master Trainers + if ( current_user_can( 'view_master_dashboard' ) || current_user_can( 'view_all_trainer_data' ) ) { + // Master Trainers go to the Master Dashboard + $master_dashboard_url = home_url( '/master-trainer/dashboard/' ); + wp_safe_redirect( $master_dashboard_url ); + exit; + } elseif ( in_array( 'hvac_trainer', (array) $user->roles ) || current_user_can( 'view_hvac_dashboard' ) ) { + // Regular HVAC trainers go to their dashboard + $dashboard_url = home_url( '/trainer/dashboard/' ); + wp_safe_redirect( $dashboard_url ); + exit; + } elseif ( current_user_can( 'manage_options' ) ) { + // Administrators can choose - redirect to WP admin or allow access to dashboard + // For now, let them stay on the login page with a message, or redirect to admin + $admin_url = admin_url(); + wp_safe_redirect( $admin_url ); + exit; + } else { + // Other logged-in users get redirected to home page + wp_safe_redirect( home_url() ); + exit; + } + } + } + +} \ No newline at end of file diff --git a/includes/community/class-order-summary-data.php b/includes/community/class-order-summary-data.php new file mode 100644 index 00000000..c8fcebc6 --- /dev/null +++ b/includes/community/class-order-summary-data.php @@ -0,0 +1,343 @@ +order_id = absint( $order_id ); + $this->order_object = $this->load_order_object( $this->order_id ); + + // Load associated events + if ($this->is_valid_order()) { + $this->event_ids = $this->get_associated_events(); + } + } + + /** + * Load the order object based on the order ID. + * + * @param int $order_id + * @return object|null + */ + private function load_order_object( $order_id ) { + // WooCommerce order + if ( class_exists( 'WC_Order' ) && function_exists( 'wc_get_order' ) ) { + $order = wc_get_order( $order_id ); + if ( $order ) { + return $order; + } + } + + // Event Tickets RSVP/Tribe order (fallback) + if ( class_exists( 'Tribe__Tickets__RSVP' ) ) { + // Implementation depends on how RSVP orders are stored + // This is a placeholder for potential RSVP orders + } + + // Add additional logic for other ticket providers if needed + return null; + } + + /** + * Check if the order is valid. + * + * @return bool + */ + public function is_valid_order() { + return ! is_null( $this->order_object ); + } + + /** + * Check if the current user has permission to view this order. + * Users can only view orders for events they created. + * + * @return bool + */ + public function user_can_view_order() { + // User must be logged in + if (!is_user_logged_in()) { + return false; + } + + // Order must be valid + if (!$this->is_valid_order()) { + return false; + } + + // Admin users can view all orders + if (current_user_can('manage_options')) { + return true; + } + + // Get the current user ID + $current_user_id = get_current_user_id(); + + // Check if the user is the author of any of the events in this order + foreach ($this->event_ids as $event_id) { + $event = get_post($event_id); + if ($event && $event->post_author == $current_user_id) { + return true; + } + } + + return false; + } + + /** + * Get event IDs associated with this order. + * + * @return array Array of event IDs + */ + public function get_associated_events() { + $event_ids = []; + + // Get attendees for this order + $attendees = []; + if (function_exists('tribe_tickets_get_order_attendees')) { + $attendees = tribe_tickets_get_order_attendees($this->order_id); + } + + // Extract event IDs from attendees + foreach ($attendees as $attendee) { + if (isset($attendee['event_id'])) { + $event_ids[] = absint($attendee['event_id']); + } + } + + return array_unique($event_ids); + } + + /** + * Get basic order details. + * + * @return array|null + */ + public function get_order_details() { + if ( ! $this->is_valid_order() ) { + return null; + } + + $details = [ + 'order_id' => $this->order_id, + 'order_number' => null, + 'purchaser_name'=> null, + 'purchaser_email'=> null, + 'purchase_date' => null, + 'total_price' => null, + 'status' => null, + 'tickets' => [], + 'events' => [], + 'billing_address' => null, + 'payment_method' => null, + 'organization' => null, + ]; + + // WooCommerce order details + if ( $this->order_object instanceof WC_Order ) { + $details['order_number'] = $this->order_object->get_order_number(); + $details['purchaser_name'] = $this->order_object->get_billing_first_name() . ' ' . $this->order_object->get_billing_last_name(); + $details['purchaser_email']= $this->order_object->get_billing_email(); + $details['purchase_date'] = $this->order_object->get_date_created() ? $this->order_object->get_date_created()->date( 'Y-m-d H:i:s' ) : null; + $details['total_price'] = $this->order_object->get_formatted_order_total(); + $details['status'] = $this->order_object->get_status(); + $details['tickets'] = $this->get_order_tickets(); + $details['events'] = $this->get_event_details(); + + // Get billing address + $address_parts = [ + $this->order_object->get_billing_address_1(), + $this->order_object->get_billing_address_2(), + $this->order_object->get_billing_city(), + $this->order_object->get_billing_state(), + $this->order_object->get_billing_postcode(), + $this->order_object->get_billing_country() + ]; + + // Filter out empty address parts and join + $address_parts = array_filter($address_parts); + $details['billing_address'] = implode(', ', $address_parts); + + // Get payment method + $details['payment_method'] = $this->order_object->get_payment_method_title(); + + // Get organization (company name) + $details['organization'] = $this->order_object->get_billing_company(); + } + + // Add additional providers here if needed + + return $details; + } + + /** + * Get ticket/attendee information for the order. + * + * @return array + */ + public function get_order_tickets() { + $tickets = []; + + // WooCommerce + Event Tickets Plus + if ( $this->order_object instanceof WC_Order && function_exists( 'tribe_tickets_get_order_attendees' ) ) { + $order_id = $this->order_id; + $attendees = tribe_tickets_get_order_attendees( $order_id ); + + foreach ( $attendees as $attendee ) { + $event_id = $attendee['event_id'] ?? null; + $event_title = ''; + + if ($event_id) { + $event_title = get_the_title($event_id); + } + + $tickets[] = [ + 'attendee_id' => $attendee['attendee_id'] ?? null, + 'ticket_type' => $attendee['ticket_name'] ?? null, + 'ticket_type_id' => $attendee['product_id'] ?? null, + 'attendee_name' => $attendee['holder_name'] ?? null, + 'attendee_email' => $attendee['holder_email'] ?? null, + 'security_code' => $attendee['security_code'] ?? null, + 'checked_in' => isset( $attendee['check_in'] ) ? (bool) $attendee['check_in'] : false, + 'event_id' => $event_id, + 'event_title' => $event_title, + 'price' => $attendee['price'] ?? $attendee['price_paid'] ?? null, + 'additional_fields' => $this->get_attendee_additional_fields($attendee), + ]; + } + } + + // Add additional providers here if needed + + return $tickets; + } + + /** + * Get details of events associated with this order. + * + * @return array + */ + public function get_event_details() { + $events = []; + + foreach ($this->event_ids as $event_id) { + $event = get_post($event_id); + if (!$event) { + continue; + } + + $event_data = [ + 'id' => $event_id, + 'title' => $event->post_title, + 'permalink' => get_permalink($event_id), + 'start_date' => null, + 'end_date' => null, + 'venue' => null, + ]; + + // Add Event Calendar specific data if available + if (function_exists('tribe_get_start_date')) { + $event_data['start_date'] = tribe_get_start_date($event_id, false); + $event_data['end_date'] = tribe_get_end_date($event_id, false); + + if (function_exists('tribe_get_venue')) { + $event_data['venue'] = tribe_get_venue($event_id); + } + } + + $events[] = $event_data; + } + + return $events; + } + + /** + * Get additional fields for an attendee. + * These could be custom fields collected during checkout. + * + * @param array $attendee The attendee data + * @return array + */ + private function get_attendee_additional_fields($attendee) { + $additional_fields = []; + + // Check for meta data stored with the attendee + if (isset($attendee['attendee_meta']) && is_array($attendee['attendee_meta'])) { + foreach ($attendee['attendee_meta'] as $key => $value) { + // Skip internal or empty fields + if (strpos($key, '_') === 0 || empty($value)) { + continue; + } + + // Format field name for display + $field_name = ucwords(str_replace(['_', '-'], ' ', $key)); + + $additional_fields[$key] = [ + 'label' => $field_name, + 'value' => $value + ]; + } + } + + return $additional_fields; + } + + /** + * Get order notes. + * + * @return array + */ + public function get_order_notes() { + $notes = []; + + if ($this->order_object instanceof WC_Order && function_exists('wc_get_order_notes')) { + $raw_notes = wc_get_order_notes([ + 'order_id' => $this->order_id, + 'type' => 'customer', + ]); + + foreach ($raw_notes as $note) { + $notes[] = [ + 'id' => $note->id, + 'content' => $note->content, + 'date' => $note->date_created->date('Y-m-d H:i:s'), + 'author' => $note->added_by, + ]; + } + } + + return $notes; + } +} \ No newline at end of file diff --git a/includes/database/class-hvac-contact-submissions-table.php b/includes/database/class-hvac-contact-submissions-table.php new file mode 100644 index 00000000..b21f818d --- /dev/null +++ b/includes/database/class-hvac-contact-submissions-table.php @@ -0,0 +1,299 @@ +prefix . self::$table_name; + } + + /** + * Create the contact submissions table + * + * @return void + */ + public static function create_table() { + global $wpdb; + + $table_name = self::get_table_name(); + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE IF NOT EXISTS $table_name ( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + trainer_id BIGINT(20) UNSIGNED NOT NULL, + trainer_profile_id BIGINT(20) UNSIGNED NOT NULL, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + email VARCHAR(255) NOT NULL, + phone VARCHAR(20), + city VARCHAR(100), + state_province VARCHAR(100), + company VARCHAR(255), + message TEXT, + ip_address VARCHAR(45), + user_agent TEXT, + submission_date DATETIME DEFAULT CURRENT_TIMESTAMP, + status ENUM('new', 'read', 'replied', 'archived') DEFAULT 'new', + notes TEXT, + PRIMARY KEY (id), + KEY trainer_id (trainer_id), + KEY trainer_profile_id (trainer_profile_id), + KEY status (status), + KEY submission_date (submission_date), + KEY email (email) + ) $charset_collate;"; + + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + dbDelta($sql); + + // Store version for future upgrades + update_option('hvac_contact_submissions_db_version', '1.0.0'); + } + + /** + * Drop the table + * + * @return void + */ + public static function drop_table() { + global $wpdb; + $table_name = self::get_table_name(); + $wpdb->query("DROP TABLE IF EXISTS $table_name"); + delete_option('hvac_contact_submissions_db_version'); + } + + /** + * Insert a new contact submission + * + * @param array $data Submission data + * @return int|false Insert ID or false on failure + */ + public static function insert_submission($data) { + global $wpdb; + + $defaults = [ + 'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '', + 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', + 'submission_date' => current_time('mysql'), + 'status' => 'new' + ]; + + $data = wp_parse_args($data, $defaults); + + // Sanitize data + $data = array_map(function($value) { + if (is_string($value)) { + return sanitize_text_field($value); + } + return $value; + }, $data); + + // Special handling for email + $data['email'] = sanitize_email($data['email']); + + // Special handling for message + if (isset($data['message'])) { + $data['message'] = sanitize_textarea_field($data['message']); + } + + $result = $wpdb->insert( + self::get_table_name(), + $data, + [ + '%d', // trainer_id + '%d', // trainer_profile_id + '%s', // first_name + '%s', // last_name + '%s', // email + '%s', // phone + '%s', // city + '%s', // state_province + '%s', // company + '%s', // message + '%s', // ip_address + '%s', // user_agent + '%s', // submission_date + '%s', // status + '%s' // notes + ] + ); + + if ($result === false) { + error_log('HVAC Contact Submission Error: ' . $wpdb->last_error); + return false; + } + + return $wpdb->insert_id; + } + + /** + * Get submissions based on criteria + * + * @param array $args Query arguments + * @return array + */ + public static function get_submissions($args = []) { + global $wpdb; + + $defaults = [ + 'trainer_id' => null, + 'status' => null, + 'limit' => 20, + 'offset' => 0, + 'orderby' => 'submission_date', + 'order' => 'DESC' + ]; + + $args = wp_parse_args($args, $defaults); + + $table_name = self::get_table_name(); + $where = []; + $where_values = []; + + if ($args['trainer_id']) { + $where[] = 'trainer_id = %d'; + $where_values[] = $args['trainer_id']; + } + + if ($args['status']) { + $where[] = 'status = %s'; + $where_values[] = $args['status']; + } + + $where_clause = ''; + if (!empty($where)) { + $where_clause = 'WHERE ' . implode(' AND ', $where); + } + + $orderby = in_array($args['orderby'], ['submission_date', 'id', 'status']) ? $args['orderby'] : 'submission_date'; + $order = in_array($args['order'], ['ASC', 'DESC']) ? $args['order'] : 'DESC'; + + $query = "SELECT * FROM $table_name $where_clause ORDER BY $orderby $order LIMIT %d OFFSET %d"; + $where_values[] = $args['limit']; + $where_values[] = $args['offset']; + + if (!empty($where_values)) { + $query = $wpdb->prepare($query, $where_values); + } + + return $wpdb->get_results($query); + } + + /** + * Get submission by ID + * + * @param int $id Submission ID + * @return object|null + */ + public static function get_submission($id) { + global $wpdb; + + return $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM %s WHERE id = %d", + self::get_table_name(), + $id + ) + ); + } + + /** + * Update submission status + * + * @param int $id Submission ID + * @param string $status New status + * @return bool + */ + public static function update_status($id, $status) { + global $wpdb; + + $valid_statuses = ['new', 'read', 'replied', 'archived']; + if (!in_array($status, $valid_statuses)) { + return false; + } + + return $wpdb->update( + self::get_table_name(), + ['status' => $status], + ['id' => $id], + ['%s'], + ['%d'] + ) !== false; + } + + /** + * Get submission count by trainer + * + * @param int $trainer_id Trainer user ID + * @param string $status Optional status filter + * @return int + */ + public static function get_submission_count($trainer_id, $status = null) { + global $wpdb; + + $table_name = self::get_table_name(); + + if ($status) { + return $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM $table_name WHERE trainer_id = %d AND status = %s", + $trainer_id, + $status + ) + ); + } + + return $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM $table_name WHERE trainer_id = %d", + $trainer_id + ) + ); + } + + /** + * Clean old submissions + * + * @param int $days Number of days to keep + * @return int Number of deleted rows + */ + public static function clean_old_submissions($days = 90) { + global $wpdb; + + $table_name = self::get_table_name(); + $cutoff_date = date('Y-m-d H:i:s', strtotime("-{$days} days")); + + return $wpdb->query( + $wpdb->prepare( + "DELETE FROM $table_name WHERE submission_date < %s AND status = 'archived'", + $cutoff_date + ) + ); + } +} \ No newline at end of file diff --git a/includes/find-trainer/class-hvac-contact-form-handler.php b/includes/find-trainer/class-hvac-contact-form-handler.php new file mode 100644 index 00000000..6b50eae6 --- /dev/null +++ b/includes/find-trainer/class-hvac-contact-form-handler.php @@ -0,0 +1,603 @@ +init_hooks(); + } + + /** + * Initialize hooks + */ + private function init_hooks() { + // AJAX handlers + add_action('wp_ajax_hvac_submit_contact_form', [$this, 'ajax_submit_form']); + add_action('wp_ajax_nopriv_hvac_submit_contact_form', [$this, 'ajax_submit_form']); + + // Admin hooks + add_action('admin_menu', [$this, 'add_admin_menu']); + add_action('admin_init', [$this, 'register_settings']); + + // Cron job for cleanup + add_action('hvac_cleanup_old_submissions', [$this, 'cleanup_old_submissions']); + + if (!wp_next_scheduled('hvac_cleanup_old_submissions')) { + wp_schedule_event(time(), 'daily', 'hvac_cleanup_old_submissions'); + } + } + + /** + * AJAX handler for form submission + */ + public function ajax_submit_form() { + check_ajax_referer('hvac_find_trainer', 'nonce'); + + $form_data = [ + 'trainer_id' => intval($_POST['trainer_id'] ?? 0), + 'trainer_profile_id' => intval($_POST['trainer_profile_id'] ?? 0), + 'first_name' => sanitize_text_field($_POST['first_name'] ?? ''), + 'last_name' => sanitize_text_field($_POST['last_name'] ?? ''), + 'email' => sanitize_email($_POST['email'] ?? ''), + 'phone' => sanitize_text_field($_POST['phone'] ?? ''), + 'city' => sanitize_text_field($_POST['city'] ?? ''), + 'state_province' => sanitize_text_field($_POST['state_province'] ?? ''), + 'company' => sanitize_text_field($_POST['company'] ?? ''), + 'message' => sanitize_textarea_field($_POST['message'] ?? '') + ]; + + // Validate form data + $validation = $this->validate_form_data($form_data); + + if (!$validation['valid']) { + wp_send_json_error([ + 'message' => 'Please correct the following errors:', + 'errors' => $validation['errors'] + ]); + } + + // Check rate limiting + if (!$this->check_submission_rate_limit($form_data['email'])) { + wp_send_json_error([ + 'message' => 'You have reached the submission limit. Please try again later.' + ]); + } + + // Save submission + $submission_id = $this->save_submission($form_data); + + if (!$submission_id) { + wp_send_json_error([ + 'message' => 'An error occurred while saving your submission. Please try again.' + ]); + } + + // Send notifications + $this->send_notifications($submission_id); + + wp_send_json_success([ + 'message' => 'Your message has been sent successfully! The trainer will contact you soon.', + 'submission_id' => $submission_id + ]); + } + + /** + * Validate form data + * + * @param array $data Form data + * @return array Validation result + */ + public function validate_form_data($data) { + $errors = []; + $valid = true; + + // Required fields + $required_fields = [ + 'trainer_id' => 'Trainer ID', + 'trainer_profile_id' => 'Trainer Profile ID', + 'first_name' => 'First Name', + 'last_name' => 'Last Name', + 'email' => 'Email' + ]; + + foreach ($required_fields as $field => $label) { + if (empty($data[$field])) { + $errors[$field] = $label . ' is required.'; + $valid = false; + } + } + + // Validate email format + if (!empty($data['email']) && !is_email($data['email'])) { + $errors['email'] = 'Please enter a valid email address.'; + $valid = false; + } + + // Validate phone format (optional) + if (!empty($data['phone'])) { + $phone = preg_replace('/[^0-9+()-.\s]/', '', $data['phone']); + if (strlen($phone) < 10) { + $errors['phone'] = 'Please enter a valid phone number.'; + $valid = false; + } + } + + // Validate trainer exists + if (!empty($data['trainer_id'])) { + $trainer = get_userdata($data['trainer_id']); + if (!$trainer || !in_array('hvac_trainer', $trainer->roles) && !in_array('hvac_master_trainer', $trainer->roles)) { + $errors['trainer_id'] = 'Invalid trainer selected.'; + $valid = false; + } + } + + // Validate trainer profile exists + if (!empty($data['trainer_profile_id'])) { + $profile = get_post($data['trainer_profile_id']); + if (!$profile || $profile->post_type !== 'trainer_profile') { + $errors['trainer_profile_id'] = 'Invalid trainer profile.'; + $valid = false; + } + } + + // Message length + if (!empty($data['message']) && strlen($data['message']) > 5000) { + $errors['message'] = 'Message is too long (maximum 5000 characters).'; + $valid = false; + } + + return [ + 'valid' => $valid, + 'errors' => $errors + ]; + } + + /** + * Check submission rate limit + * + * @param string $email Email address + * @return bool True if within limits + */ + public function check_submission_rate_limit($email) { + $transient_key = 'hvac_contact_' . md5($email); + $submissions = get_transient($transient_key); + + if ($submissions === false) { + $submissions = 0; + } + + if ($submissions >= self::RATE_LIMIT_SUBMISSIONS) { + return false; + } + + set_transient($transient_key, $submissions + 1, self::RATE_LIMIT_WINDOW); + return true; + } + + /** + * Save submission to database + * + * @param array $data Form data + * @return int|false Submission ID or false on failure + */ + public function save_submission($data) { + // Include the database table class + if (!class_exists('HVAC_Contact_Submissions_Table')) { + require_once HVAC_PLUGIN_DIR . 'includes/database/class-hvac-contact-submissions-table.php'; + } + + return HVAC_Contact_Submissions_Table::insert_submission($data); + } + + /** + * Send notifications for new submission + * + * @param int $submission_id Submission ID + */ + public function send_notifications($submission_id) { + global $wpdb; + + $table_name = $wpdb->prefix . 'hvac_contact_submissions'; + $submission = $wpdb->get_row( + $wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $submission_id) + ); + + if (!$submission) { + return; + } + + // Get trainer email + $trainer = get_userdata($submission->trainer_id); + if (!$trainer) { + return; + } + + // Send email to trainer + $this->send_trainer_notification($trainer, $submission); + + // Send confirmation to submitter + $this->send_submitter_confirmation($submission); + + // Send admin notification if enabled + if (get_option('hvac_contact_admin_notifications', false)) { + $this->send_admin_notification($submission); + } + } + + /** + * Send notification email to trainer + * + * @param WP_User $trainer Trainer user object + * @param object $submission Submission data + */ + private function send_trainer_notification($trainer, $submission) { + $subject = sprintf( + 'New Contact Request from %s %s', + $submission->first_name, + $submission->last_name + ); + + $message = $this->get_email_template('trainer_notification', [ + 'trainer_name' => $trainer->display_name, + 'submitter_name' => $submission->first_name . ' ' . $submission->last_name, + 'submitter_email' => $submission->email, + 'submitter_phone' => $submission->phone, + 'submitter_city' => $submission->city, + 'submitter_state' => $submission->state_province, + 'submitter_company' => $submission->company, + 'submitter_message' => $submission->message, + 'submission_date' => $submission->submission_date, + 'dashboard_url' => home_url('/trainer/dashboard/') + ]); + + $headers = [ + 'Content-Type: text/html; charset=UTF-8', + 'From: ' . get_bloginfo('name') . ' <' . get_option('admin_email') . '>', + 'Reply-To: ' . $submission->first_name . ' ' . $submission->last_name . ' <' . $submission->email . '>' + ]; + + wp_mail($trainer->user_email, $subject, $message, $headers); + } + + /** + * Send confirmation email to submitter + * + * @param object $submission Submission data + */ + private function send_submitter_confirmation($submission) { + $trainer = get_userdata($submission->trainer_id); + if (!$trainer) { + return; + } + + $subject = 'Your message has been sent to ' . $trainer->display_name; + + $message = $this->get_email_template('submitter_confirmation', [ + 'submitter_name' => $submission->first_name, + 'trainer_name' => $trainer->display_name, + 'message_copy' => $submission->message, + 'submission_date' => $submission->submission_date + ]); + + $headers = [ + 'Content-Type: text/html; charset=UTF-8', + 'From: ' . get_bloginfo('name') . ' <' . get_option('admin_email') . '>' + ]; + + wp_mail($submission->email, $subject, $message, $headers); + } + + /** + * Send admin notification + * + * @param object $submission Submission data + */ + private function send_admin_notification($submission) { + $admin_email = get_option('hvac_contact_admin_email', get_option('admin_email')); + $trainer = get_userdata($submission->trainer_id); + + $subject = 'New Contact Form Submission on Find a Trainer'; + + $message = $this->get_email_template('admin_notification', [ + 'trainer_name' => $trainer ? $trainer->display_name : 'Unknown', + 'submitter_name' => $submission->first_name . ' ' . $submission->last_name, + 'submitter_email' => $submission->email, + 'submitter_phone' => $submission->phone, + 'submitter_company' => $submission->company, + 'submission_date' => $submission->submission_date, + 'admin_url' => admin_url('admin.php?page=hvac-contact-submissions') + ]); + + $headers = [ + 'Content-Type: text/html; charset=UTF-8', + 'From: ' . get_bloginfo('name') . ' ' + ]; + + wp_mail($admin_email, $subject, $message, $headers); + } + + /** + * Get email template + * + * @param string $template Template name + * @param array $variables Template variables + * @return string Email HTML + */ + private function get_email_template($template, $variables = []) { + $template_file = HVAC_PLUGIN_DIR . 'templates/emails/' . $template . '.php'; + + if (!file_exists($template_file)) { + // Use default template + return $this->get_default_email_template($template, $variables); + } + + extract($variables); + ob_start(); + include $template_file; + return ob_get_clean(); + } + + /** + * Get default email template + * + * @param string $template Template name + * @param array $vars Template variables + * @return string Email HTML + */ + private function get_default_email_template($template, $vars) { + $html = ''; + $html .= '
'; + + switch ($template) { + case 'trainer_notification': + $html .= '

New Contact Request

'; + $html .= '

Hello ' . esc_html($vars['trainer_name']) . ',

'; + $html .= '

You have received a new contact request through the Find a Trainer directory.

'; + $html .= '

Contact Details:

'; + $html .= '
    '; + $html .= '
  • Name: ' . esc_html($vars['submitter_name']) . '
  • '; + $html .= '
  • Email: ' . esc_html($vars['submitter_email']) . '
  • '; + if ($vars['submitter_phone']) { + $html .= '
  • Phone: ' . esc_html($vars['submitter_phone']) . '
  • '; + } + if ($vars['submitter_company']) { + $html .= '
  • Company: ' . esc_html($vars['submitter_company']) . '
  • '; + } + if ($vars['submitter_city'] || $vars['submitter_state']) { + $html .= '
  • Location: ' . esc_html($vars['submitter_city']) . ', ' . esc_html($vars['submitter_state']) . '
  • '; + } + $html .= '
'; + if ($vars['submitter_message']) { + $html .= '

Message:

'; + $html .= '

' . nl2br(esc_html($vars['submitter_message'])) . '

'; + } + $html .= '

View in Dashboard

'; + break; + + case 'submitter_confirmation': + $html .= '

Message Sent Successfully

'; + $html .= '

Hello ' . esc_html($vars['submitter_name']) . ',

'; + $html .= '

Your message has been successfully sent to ' . esc_html($vars['trainer_name']) . '. They will contact you soon.

'; + if ($vars['message_copy']) { + $html .= '

Your Message:

'; + $html .= '

' . nl2br(esc_html($vars['message_copy'])) . '

'; + } + $html .= '

Thank you for using our Find a Trainer directory!

'; + break; + + case 'admin_notification': + $html .= '

New Contact Form Submission

'; + $html .= '

A new contact form has been submitted on the Find a Trainer page.

'; + $html .= '

Details:

'; + $html .= '
    '; + $html .= '
  • Trainer: ' . esc_html($vars['trainer_name']) . '
  • '; + $html .= '
  • From: ' . esc_html($vars['submitter_name']) . '
  • '; + $html .= '
  • Email: ' . esc_html($vars['submitter_email']) . '
  • '; + if ($vars['submitter_phone']) { + $html .= '
  • Phone: ' . esc_html($vars['submitter_phone']) . '
  • '; + } + if ($vars['submitter_company']) { + $html .= '
  • Company: ' . esc_html($vars['submitter_company']) . '
  • '; + } + $html .= '
  • Date: ' . esc_html($vars['submission_date']) . '
  • '; + $html .= '
'; + $html .= '

View All Submissions

'; + break; + } + + $html .= '
'; + $html .= '

This is an automated message from ' . get_bloginfo('name') . '

'; + $html .= '
'; + + return $html; + } + + /** + * Add admin menu for contact submissions + */ + public function add_admin_menu() { + add_submenu_page( + 'hvac-plugin', + 'Contact Submissions', + 'Contact Submissions', + 'manage_options', + 'hvac-contact-submissions', + [$this, 'render_admin_page'] + ); + } + + /** + * Register plugin settings + */ + public function register_settings() { + register_setting('hvac_contact_settings', 'hvac_contact_admin_notifications'); + register_setting('hvac_contact_settings', 'hvac_contact_admin_email'); + register_setting('hvac_contact_settings', 'hvac_contact_retention_days'); + } + + /** + * Render admin page for contact submissions + */ + public function render_admin_page() { + if (!class_exists('HVAC_Contact_Submissions_Table')) { + require_once HVAC_PLUGIN_DIR . 'includes/database/class-hvac-contact-submissions-table.php'; + } + + // Handle status updates + if (isset($_POST['update_status']) && isset($_POST['submission_id'])) { + check_admin_referer('hvac_update_submission_status'); + + $submission_id = intval($_POST['submission_id']); + $new_status = sanitize_text_field($_POST['new_status']); + + HVAC_Contact_Submissions_Table::update_status($submission_id, $new_status); + + echo '

Status updated successfully!

'; + } + + // Get submissions + $args = [ + 'limit' => 50, + 'offset' => (get_query_var('paged', 1) - 1) * 50 + ]; + + if (isset($_GET['status'])) { + $args['status'] = sanitize_text_field($_GET['status']); + } + + $submissions = HVAC_Contact_Submissions_Table::get_submissions($args); + + ?> +
+

Contact Submissions

+ +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + trainer_id); + ?> + + + + + + + + + + + + + + + + + +
IDDateFromEmailTrainerMessageStatusActions
id); ?>submission_date))); ?>first_name . ' ' . $submission->last_name); ?>email); ?>display_name) : 'Unknown'; ?>message, 0, 100)) . (strlen($submission->message) > 100 ? '...' : ''); ?> + + status)); ?> + + + +
No submissions found.
+
+ + + 0) { + error_log("HVAC Contact Form: Cleaned up $deleted old submissions"); + } + } +} \ No newline at end of file diff --git a/includes/find-trainer/class-hvac-find-trainer-page.php b/includes/find-trainer/class-hvac-find-trainer-page.php new file mode 100644 index 00000000..5e19e21f --- /dev/null +++ b/includes/find-trainer/class-hvac-find-trainer-page.php @@ -0,0 +1,570 @@ +init_hooks(); + } + + /** + * Initialize hooks + */ + private function init_hooks() { + add_action('init', [$this, 'register_page']); + add_action('wp_enqueue_scripts', [$this, 'enqueue_assets']); + add_filter('body_class', [$this, 'add_body_classes']); + add_shortcode('hvac_find_trainer', [$this, 'render_shortcode']); + add_shortcode('hvac_trainer_directory', [$this, 'render_directory_shortcode']); + + // AJAX handlers + add_action('wp_ajax_hvac_get_trainer_upcoming_events', [$this, 'ajax_get_upcoming_events']); + add_action('wp_ajax_nopriv_hvac_get_trainer_upcoming_events', [$this, 'ajax_get_upcoming_events']); + } + + /** + * Register the Find a Trainer page + */ + public function register_page() { + // Check if page exists + $page = get_page_by_path($this->page_slug); + + if (!$page) { + $this->create_page(); + } + } + + /** + * Create the Find a Trainer page + */ + private function create_page() { + $page_content = $this->get_page_content(); + + $page_data = [ + 'post_title' => 'Find a Trainer', + 'post_name' => $this->page_slug, + 'post_content' => $page_content, + 'post_status' => 'publish', + 'post_type' => 'page', + 'post_author' => 1, + 'meta_input' => [ + '_wp_page_template' => 'default', + 'ast-site-content-layout' => 'page-builder', + 'site-post-title' => 'disabled', + 'site-sidebar-layout' => 'no-sidebar', + 'ast-main-header-display' => 'enabled', + 'ast-hfb-above-header-display' => 'disabled', + 'ast-hfb-below-header-display' => 'disabled', + 'ast-featured-img' => 'disabled' + ] + ]; + + $page_id = wp_insert_post($page_data); + + if ($page_id && !is_wp_error($page_id)) { + update_option('hvac_find_trainer_page_id', $page_id); + } + } + + /** + * Get the page content with Gutenberg blocks + */ + private function get_page_content() { + return ' +
+ + +
+ +

Find certified HVAC trainers in your area. Use the interactive map and filters below to discover trainers who match your specific needs. Click on any trainer to view their profile and contact them directly.

+ +
+ + + +
+ + +
+ + [display-map id="5872"] + +
+ + + +
+ +
+ +
Filters:
+ + + + +
+
+ +
+ + +
+ + + + [hvac_trainer_directory] + + + +
+ +

Are you an HVAC Trainer that wants to be listed in our directory?

+ + +
+ + + +
+ +
+ + +
+'; + } + + /** + * Enqueue assets for the Find a Trainer page + */ + public function enqueue_assets() { + if (!$this->is_find_trainer_page()) { + return; + } + + // Enqueue CSS + wp_enqueue_style( + 'hvac-find-trainer', + HVAC_PLUGIN_URL . 'assets/css/find-trainer.css', + ['astra-theme-css'], + HVAC_VERSION + ); + + // Enqueue JavaScript + wp_enqueue_script( + 'hvac-find-trainer', + HVAC_PLUGIN_URL . 'assets/js/find-trainer.js', + ['jquery'], + HVAC_VERSION, + true + ); + + // Localize script + wp_localize_script('hvac-find-trainer', 'hvac_find_trainer', [ + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('hvac_find_trainer'), + 'map_id' => '5872', + 'messages' => [ + 'loading' => __('Loading...', 'hvac'), + 'error' => __('An error occurred. Please try again.', 'hvac'), + 'no_results' => __('No trainers found matching your criteria.', 'hvac'), + 'form_error' => __('Please check the form and try again.', 'hvac'), + 'form_success' => __('Your message has been sent! Check your inbox for more details.', 'hvac') + ] + ]); + } + + /** + * Add body classes for the Find a Trainer page + */ + public function add_body_classes($classes) { + if ($this->is_find_trainer_page()) { + $classes[] = 'hvac-find-trainer-page'; + $classes[] = 'hvac-full-width'; + } + return $classes; + } + + /** + * Check if current page is the Find a Trainer page + */ + private function is_find_trainer_page() { + return is_page($this->page_slug) || is_page(get_option('hvac_find_trainer_page_id')); + } + + /** + * Render the main shortcode + */ + public function render_shortcode($atts) { + $atts = shortcode_atts([ + 'show_map' => true, + 'show_filters' => true, + 'show_directory' => true + ], $atts); + + ob_start(); + ?> +
+ +
+ +
+ + + +
+ render_filters(); ?> +
+ + + +
+ render_directory(); ?> +
+ +
+ 12, + 'columns' => 2 + ], $atts); + + ob_start(); + $this->render_directory($atts); + return ob_get_clean(); + } + + /** + * Render filter controls + */ + private function render_filters() { + ?> +
+ +
Filters:
+ + + + +
+
+ 12, + 'columns' => 2 + ]; + + $args = wp_parse_args($args, $defaults); + + // Get trainers + $query_args = [ + 'post_type' => 'trainer_profile', + 'posts_per_page' => $args['per_page'], + 'post_status' => 'publish', + 'meta_query' => [ + [ + 'key' => 'is_public_profile', + 'value' => '1', + 'compare' => '=' + ] + ] + ]; + + $trainers = new WP_Query($query_args); + + ?> +
+
+ have_posts()) : ?> + have_posts()) : $trainers->the_post(); ?> + render_trainer_card(get_the_ID()); ?> + + +
+

+
+ +
+ + max_num_pages > 1) : ?> +
+ $trainers->max_num_pages, + 'current' => max(1, get_query_var('paged')), + 'prev_text' => '« Previous', + 'next_text' => 'Next »' + ]); + ?> +
+ +
+ +
+
+
+ + <?php echo esc_attr($trainer_name); ?> + +
+ +
+ +
+ +
+

+ + + +

+

+ , +

+ +

+ +

+ +
+ +
+
+
+
+ 'trainer_profile', + 'posts_per_page' => -1, + 'post_status' => 'publish', + 'meta_query' => [ + [ + 'key' => 'is_public_profile', + 'value' => '1', + 'compare' => '=' + ], + [ + 'key' => 'latitude', + 'compare' => 'EXISTS' + ], + [ + 'key' => 'longitude', + 'compare' => 'EXISTS' + ] + ] + ]; + + $trainers = new WP_Query($args); + + if ($trainers->have_posts()) { + while ($trainers->have_posts()) { + $trainers->the_post(); + $profile_id = get_the_ID(); + + $markers[] = $this->format_marker_data($profile_id); + } + } + + wp_reset_postdata(); + + return $markers; + } + + /** + * Format marker data for a trainer + */ + private function format_marker_data($profile_id) { + $user_id = get_post_meta($profile_id, 'user_id', true); + $trainer_name = get_post_meta($profile_id, 'trainer_display_name', true); + $city = get_post_meta($profile_id, 'trainer_city', true); + $state = get_post_meta($profile_id, 'trainer_state', true); + $lat = get_post_meta($profile_id, 'latitude', true); + $lng = get_post_meta($profile_id, 'longitude', true); + $certification = get_post_meta($profile_id, 'certification_type', true); + $business_types = wp_get_post_terms($profile_id, 'business_type', ['fields' => 'names']); + + return [ + 'id' => $profile_id, + 'title' => $trainer_name, + 'lat' => floatval($lat), + 'lng' => floatval($lng), + 'content' => $this->generate_marker_content($profile_id), + 'data' => [ + 'trainer_id' => $user_id, + 'profile_id' => $profile_id, + 'certification' => $certification, + 'business_type' => $business_types, + 'state' => $state, + 'city' => $city + ] + ]; + } + + /** + * Generate marker popup content + */ + private function generate_marker_content($profile_id) { + $trainer_name = get_post_meta($profile_id, 'trainer_display_name', true); + $city = get_post_meta($profile_id, 'trainer_city', true); + $state = get_post_meta($profile_id, 'trainer_state', true); + + return sprintf( + '
+

%s

+

%s, %s

+ +
', + $profile_id, + esc_html($trainer_name), + esc_html($city), + esc_html($state), + $profile_id + ); + } + + /** + * AJAX handler for getting trainer upcoming events + */ + public function ajax_get_upcoming_events() { + // Verify nonce + if (!wp_verify_nonce($_POST['nonce'], 'hvac_find_trainer')) { + wp_send_json_error('Invalid nonce'); + } + + $profile_id = intval($_POST['profile_id']); + if (!$profile_id) { + wp_send_json_error('Invalid profile ID'); + } + + // Get the user ID from the profile + $user_id = get_post_meta($profile_id, 'user_id', true); + if (!$user_id) { + wp_send_json_error('No user found for this profile'); + } + + $upcoming_events = []; + + // Get upcoming events using the fixed query + if (function_exists('tribe_get_events')) { + $events = tribe_get_events([ + 'author' => $user_id, + 'posts_per_page' => 5, + 'ends_after' => 'now', + 'orderby' => 'event_date', + 'order' => 'ASC' + ]); + + foreach ($events as $event) { + $upcoming_events[] = [ + 'title' => $event->post_title, + 'date' => tribe_get_start_date($event->ID, false, 'M j, Y'), + 'url' => get_permalink($event->ID) + ]; + } + } + + wp_send_json_success([ + 'events' => $upcoming_events, + 'count' => count($upcoming_events) + ]); + } +} \ No newline at end of file diff --git a/includes/find-trainer/class-hvac-mapgeo-integration.php b/includes/find-trainer/class-hvac-mapgeo-integration.php new file mode 100644 index 00000000..aa292589 --- /dev/null +++ b/includes/find-trainer/class-hvac-mapgeo-integration.php @@ -0,0 +1,1419 @@ +init_hooks(); + } + + /** + * Check if we're in production environment + */ + private function is_production() { + // Check if we're on upskillhvac.com domain (production) + $site_url = get_site_url(); + return strpos($site_url, 'upskillhvac.com') !== false; + } + + /** + * Debug logging wrapper + */ + private function debug_log($message) { + if (!$this->is_production() && defined('WP_DEBUG') && WP_DEBUG) { + error_log('HVAC MapGeo: ' . $message); + } + } + + /** + * Initialize hooks + */ + private function init_hooks() { + // Hook into MapGeo data filter - modify layout and inject trainer modal data + add_filter('igm_add_meta', [$this, 'modify_map_layout'], 10, 2); + + // Hook into MapGeo marker data to add our trainer information + add_filter('igm_marker_data', [$this, 'inject_trainer_modal_data'], 10, 2); + + // AJAX handlers for our own filtering + add_action('wp_ajax_hvac_filter_trainers', [$this, 'ajax_filter_trainers']); + add_action('wp_ajax_nopriv_hvac_filter_trainers', [$this, 'ajax_filter_trainers']); + + // AJAX handler to get trainer certification type for Champions check + add_action('wp_ajax_hvac_get_trainer_certification', [$this, 'ajax_get_trainer_certification']); + add_action('wp_ajax_nopriv_hvac_get_trainer_certification', [$this, 'ajax_get_trainer_certification']); + + // AJAX handler to get complete trainer profile data + add_action('wp_ajax_hvac_get_trainer_profile', [$this, 'ajax_get_trainer_profile']); + add_action('wp_ajax_nopriv_hvac_get_trainer_profile', [$this, 'ajax_get_trainer_profile']); + + add_action('wp_ajax_hvac_search_trainers', [$this, 'ajax_search_trainers']); + add_action('wp_ajax_nopriv_hvac_search_trainers', [$this, 'ajax_search_trainers']); + + // Add JavaScript to handle MapGeo marker clicks + add_action('wp_footer', [$this, 'add_mapgeo_click_handlers']); + } + + /** + * Debug MapGeo integration + */ + public function debug_mapgeo_integration() { + if (isset($_GET['debug_mapgeo']) && current_user_can('manage_options')) { + $this->debug_log('Debug: Integration loaded'); + $this->debug_log('Debug: Filters registered for igm_add_meta'); + } + } + + /** + * Modify map layout to prevent sidebar display and inject trainer profile IDs + * + * @param array $meta Map metadata + * @param int $map_id Map ID + * @return array Modified metadata + */ + public function modify_map_layout($meta, $map_id = null) { + // Only process if meta is an array + if (!is_array($meta)) { + return $meta; + } + + // Only process for our specific map (5872) + if ($map_id && $map_id != $this->map_id) { + return $meta; + } + + error_log('HVAC MapGeo: Processing map layout modification for map ' . $map_id); + error_log('HVAC MapGeo: Meta keys: ' . implode(', ', array_keys($meta))); + error_log('HVAC MapGeo: Full meta structure: ' . print_r($meta, true)); + + // Check for different marker types that MapGeo might use + $marker_types = ['roundMarkers', 'iconMarkers', 'markers', 'customMarkers']; + + foreach ($marker_types as $marker_type) { + if (isset($meta[$marker_type]) && is_array($meta[$marker_type])) { + error_log('HVAC MapGeo: Found ' . count($meta[$marker_type]) . ' markers of type: ' . $marker_type); + + foreach ($meta[$marker_type] as $index => &$marker) { + // Log marker structure for debugging + error_log('HVAC MapGeo: Marker ' . $index . ' keys: ' . implode(', ', array_keys($marker))); + + // Check if this marker has trainer data we can identify + $trainer_name = null; + if (isset($marker['title']) && !empty($marker['title'])) { + $trainer_name = $marker['title']; + } elseif (isset($marker['name']) && !empty($marker['name'])) { + $trainer_name = $marker['name']; + } + + if ($trainer_name) { + error_log('HVAC MapGeo: Looking for trainer profile for: ' . $trainer_name); + + // Try to find matching trainer profile by name + $trainer_profile_id = $this->find_trainer_profile_by_name($trainer_name); + + if ($trainer_profile_id) { + error_log('HVAC MapGeo: Found profile ID ' . $trainer_profile_id . ' for trainer: ' . $trainer_name); + + // Set custom click action for trainer modal with profile ID + $marker['action'] = 'hvac_show_trainer_modal'; + $marker['hvac_profile_id'] = $trainer_profile_id; + + // Also add to marker ID for easy identification + $marker['id'] = 'trainer_' . $trainer_profile_id; + + // Remove sidebar display if it was set + if (isset($marker['action']) && $marker['action'] === 'igm_display_right_1_3') { + $marker['action'] = 'hvac_show_trainer_modal'; + } + + error_log('HVAC MapGeo: Configured marker for trainer ' . $trainer_name . ' with profile ID ' . $trainer_profile_id); + } else { + error_log('HVAC MapGeo: No profile found for trainer: ' . $trainer_name); + + // Fallback to tooltip for non-trainer markers + if (isset($marker['action']) && $marker['action'] === 'igm_display_right_1_3') { + $marker['action'] = 'tooltip'; + } + } + } else { + error_log('HVAC MapGeo: Marker ' . $index . ' has no identifiable trainer name'); + } + } + } + } + + error_log('HVAC MapGeo: Map layout modification complete'); + return $meta; + } + + /** + * Find trainer profile ID by trainer name + * + * @param string $trainer_name + * @return int|false Profile ID or false if not found + */ + private function find_trainer_profile_by_name($trainer_name) { + $args = [ + 'post_type' => 'trainer_profile', + 'posts_per_page' => 1, + 'post_status' => 'publish', + 'meta_query' => [ + [ + 'key' => 'trainer_display_name', + 'value' => $trainer_name, + 'compare' => '=' + ] + ] + ]; + + $query = new WP_Query($args); + + if ($query->have_posts()) { + return $query->posts[0]->ID; + } + + wp_reset_postdata(); + return false; + } + + /** + * AJAX handler to get trainer certification type + */ + public function ajax_get_trainer_certification() { + // Verify nonce + if (!wp_verify_nonce($_POST['nonce'], 'hvac_find_trainer')) { + wp_send_json_error('Invalid nonce'); + return; + } + + $profile_id = intval($_POST['profile_id']); + if (!$profile_id) { + wp_send_json_error('Invalid profile ID'); + return; + } + + // Get the certification type and color for this profile + $certification_type = get_post_meta($profile_id, 'certification_type', true); + $certification_color = get_post_meta($profile_id, 'certification_color', true); + + wp_send_json_success([ + 'certification_type' => $certification_type ?: 'HVAC Trainer', + 'certification_color' => $certification_color ?: '#f0f7e8' + ]); + } + + /** + * AJAX handler to get complete trainer profile data + */ + public function ajax_get_trainer_profile() { + error_log('HVAC MapGeo: ajax_get_trainer_profile called'); + + // Verify nonce + if (!wp_verify_nonce($_POST['nonce'], 'hvac_find_trainer')) { + error_log('HVAC MapGeo: Invalid nonce in ajax_get_trainer_profile'); + wp_send_json_error('Invalid nonce'); + return; + } + + $profile_id = intval($_POST['profile_id']); + error_log('HVAC MapGeo: Processing profile ID: ' . $profile_id); + + if (!$profile_id) { + error_log('HVAC MapGeo: Invalid profile ID: ' . $profile_id); + wp_send_json_error('Invalid profile ID'); + return; + } + + // Get the trainer profile post + $profile_post = get_post($profile_id); + if (!$profile_post || $profile_post->post_type !== 'trainer_profile') { + error_log('HVAC MapGeo: Invalid trainer profile - post type: ' . ($profile_post ? $profile_post->post_type : 'null')); + wp_send_json_error('Invalid trainer profile'); + return; + } + + error_log('HVAC MapGeo: Valid profile found: ' . $profile_post->post_title); + + // Get user ID for event count + $user_id = get_post_meta($profile_id, 'user_id', true); + + // Get event count + $event_count = 0; + if ($user_id && function_exists('tribe_get_events')) { + $events = tribe_get_events([ + 'author' => $user_id, + 'eventDisplay' => 'all', + 'posts_per_page' => -1, + 'fields' => 'ids' + ]); + $event_count = count($events); + } + + // Build complete trainer data structure + try { + // Get business type safely + $business_types = wp_get_post_terms($profile_id, 'business_type', ['fields' => 'names']); + $business_type = 'Independent Contractor'; + if (!is_wp_error($business_types) && !empty($business_types)) { + $business_type = implode(', ', $business_types); + } else { + $business_type_meta = get_post_meta($profile_id, 'business_type', true); + if ($business_type_meta) { + $business_type = $business_type_meta; + } + } + error_log('HVAC MapGeo: Business type: ' . $business_type); + + // Get training formats safely + $formats = wp_get_post_terms($profile_id, 'training_format', ['fields' => 'names']); + $training_formats = 'In-Person, Virtual'; + if (!is_wp_error($formats) && !empty($formats)) { + $training_formats = implode(', ', $formats); + } + error_log('HVAC MapGeo: Training formats: ' . $training_formats); + + // Get training locations safely + $resources = wp_get_post_terms($profile_id, 'training_resources', ['fields' => 'names']); + $training_locations = 'On-site, Remote'; + if (!is_wp_error($resources) && !empty($resources)) { + $training_locations = implode(', ', $resources); + } + error_log('HVAC MapGeo: Training locations: ' . $training_locations); + + $trainer_data = [ + 'profile_id' => $profile_id, + 'user_id' => $user_id, + 'name' => get_post_meta($profile_id, 'trainer_display_name', true) ?: $profile_post->post_title, + 'city' => get_post_meta($profile_id, 'trainer_city', true) ?: 'Location not available', + 'state' => get_post_meta($profile_id, 'trainer_state', true) ?: '', + 'certification_type' => get_post_meta($profile_id, 'certification_type', true) ?: 'HVAC Trainer', + 'profile_image' => get_post_meta($profile_id, 'profile_image_url', true) ?: '', + 'business_type' => $business_type, + 'event_count' => $event_count, + 'training_formats' => $training_formats, + 'training_locations' => $training_locations, + 'upcoming_events' => [] + ]; + + error_log('HVAC MapGeo: Successfully built trainer data for: ' . $trainer_data['name']); + wp_send_json_success($trainer_data); + + } catch (Exception $e) { + error_log('HVAC MapGeo: Error building trainer data: ' . $e->getMessage()); + wp_send_json_error('Error building trainer data: ' . $e->getMessage()); + } catch (Error $e) { + error_log('HVAC MapGeo: Fatal error building trainer data: ' . $e->getMessage()); + wp_send_json_error('Fatal error building trainer data: ' . $e->getMessage()); + } + } + + + /** + * Add JavaScript to handle MapGeo custom click actions + */ + public function add_mapgeo_click_handlers() { + // Only add on find trainer page + if (!is_page() || get_post_field('post_name') !== 'find-a-trainer') { + return; + } + + ?> + + + %s
+ %s, %s
+ %s
+ View Profile + ', + esc_html($trainer_name), + esc_html($city), + esc_html($state), + esc_html($certification), + $profile_id + ); + + return [ + 'id' => 'trainer_' . $profile_id, + 'coordinates' => [ + 'lat' => floatval($lat), + 'lng' => floatval($lng) + ], + 'tooltipContent' => $tooltip, + 'action' => 'tooltip', // Changed from 'none' to 'tooltip' to show tooltip on click + 'value' => '1', + 'radius' => '10', + 'fill' => $certification === 'Certified measureQuick Champion' ? '#FFD700' : '#0073aa', + 'fillOpacity' => '0.8', + 'borderColor' => '#005a87', + 'borderWidth' => '2' + ]; + } + + + /** + * Get all geocoded trainers + * + * @return array + */ + private function get_geocoded_trainers() { + $args = [ + 'post_type' => 'trainer_profile', + 'posts_per_page' => -1, + 'post_status' => 'publish', + 'meta_query' => [ + 'relation' => 'AND', + [ + 'key' => 'is_public_profile', + 'value' => '1', + 'compare' => '=' + ], + [ + 'key' => 'latitude', + 'compare' => 'EXISTS' + ], + [ + 'key' => 'longitude', + 'compare' => 'EXISTS' + ] + ] + ]; + + // Add user status filter + $this->add_user_status_filter($args); + + $query = new WP_Query($args); + $trainers = []; + + if ($query->have_posts()) { + while ($query->have_posts()) { + $query->the_post(); + $trainers[] = get_the_ID(); + } + } + + wp_reset_postdata(); + + return $trainers; + } + + /** + * Format trainer data as map marker + * + * @param int $profile_id Trainer profile post ID + * @return array Marker data + */ + private function format_trainer_marker($profile_id) { + $user_id = get_post_meta($profile_id, 'user_id', true); + $trainer_name = get_post_meta($profile_id, 'trainer_display_name', true); + $city = get_post_meta($profile_id, 'trainer_city', true); + $state = get_post_meta($profile_id, 'trainer_state', true); + $lat = get_post_meta($profile_id, 'latitude', true); + $lng = get_post_meta($profile_id, 'longitude', true); + $certification = get_post_meta($profile_id, 'certification_type', true); + $profile_image = get_post_meta($profile_id, 'profile_image_url', true); + + // Get taxonomies + $business_types = wp_get_post_terms($profile_id, 'business_type', ['fields' => 'names']); + $training_formats = wp_get_post_terms($profile_id, 'training_formats', ['fields' => 'names']); + $training_resources = wp_get_post_terms($profile_id, 'training_resources', ['fields' => 'names']); + + // Count upcoming events + $event_count = $this->get_trainer_event_count($user_id); + + return [ + 'id' => 'trainer_' . $profile_id, + 'title' => $trainer_name, + 'lat' => floatval($lat), + 'lng' => floatval($lng), + 'icon' => $this->get_marker_icon($certification), + 'content' => $this->generate_marker_popup($profile_id), + 'data' => [ + 'trainer_id' => $user_id, + 'profile_id' => $profile_id, + 'name' => $trainer_name, + 'city' => $city, + 'state' => $state, + 'certification' => $certification, + 'business_type' => $business_types, + 'training_formats' => $training_formats, + 'training_resources' => $training_resources, + 'event_count' => $event_count, + 'profile_image' => $profile_image + ] + ]; + } + + /** + * Generate marker popup content + * + * @param int $profile_id Trainer profile post ID + * @return string HTML content + */ + private function generate_marker_popup($profile_id) { + $trainer_name = get_post_meta($profile_id, 'trainer_display_name', true); + $city = get_post_meta($profile_id, 'trainer_city', true); + $state = get_post_meta($profile_id, 'trainer_state', true); + $certification = get_post_meta($profile_id, 'certification_type', true); + $profile_image = get_post_meta($profile_id, 'profile_image_url', true); + + ob_start(); + ?> +
+
+ + <?php echo esc_attr($trainer_name); ?> + +
+

+

,

+ + + +
+
+
+ +
+
+ $user_id, + 'eventDisplay' => 'all', + 'posts_per_page' => -1, + 'fields' => 'ids' + ]); + + return count($events); + } + + /** + * Update marker when trainer profile is updated + * + * @param int $profile_id Trainer profile post ID + */ + public function update_trainer_marker($profile_id) { + // Clear any cached map data + delete_transient('hvac_mapgeo_markers_' . $this->map_id); + + // Trigger geocoding if needed + $lat = get_post_meta($profile_id, 'latitude', true); + $lng = get_post_meta($profile_id, 'longitude', true); + + if (empty($lat) || empty($lng)) { + $this->geocode_trainer($profile_id); + } + } + + /** + * Geocode a trainer's address + * + * @param int $profile_id Trainer profile post ID + * @return bool Success status + */ + private function geocode_trainer($profile_id) { + $address_parts = []; + + $street = get_post_meta($profile_id, 'trainer_street', true); + $city = get_post_meta($profile_id, 'trainer_city', true); + $state = get_post_meta($profile_id, 'trainer_state', true); + $zip = get_post_meta($profile_id, 'trainer_zip', true); + $country = get_post_meta($profile_id, 'trainer_country', true) ?: 'USA'; + + if ($street) $address_parts[] = $street; + if ($city) $address_parts[] = $city; + if ($state) $address_parts[] = $state; + if ($zip) $address_parts[] = $zip; + if ($country) $address_parts[] = $country; + + if (empty($address_parts)) { + return false; + } + + $address = implode(', ', $address_parts); + + // Use Google Maps Geocoding API if available + $api_key = get_option('hvac_google_maps_api_key'); + if ($api_key) { + $response = wp_remote_get('https://maps.googleapis.com/maps/api/geocode/json?' . http_build_query([ + 'address' => $address, + 'key' => $api_key + ])); + + if (!is_wp_error($response)) { + $data = json_decode(wp_remote_retrieve_body($response), true); + + if ($data['status'] === 'OK' && !empty($data['results'][0])) { + $location = $data['results'][0]['geometry']['location']; + update_post_meta($profile_id, 'latitude', $location['lat']); + update_post_meta($profile_id, 'longitude', $location['lng']); + return true; + } + } + } + + return false; + } + + /** + * Batch geocode all trainers without coordinates + */ + public function batch_geocode_trainers() { + $args = [ + 'post_type' => 'trainer_profile', + 'posts_per_page' => -1, + 'post_status' => 'publish', + 'meta_query' => [ + 'relation' => 'OR', + [ + 'key' => 'latitude', + 'compare' => 'NOT EXISTS' + ], + [ + 'key' => 'longitude', + 'compare' => 'NOT EXISTS' + ] + ] + ]; + + $query = new WP_Query($args); + $geocoded = 0; + + if ($query->have_posts()) { + while ($query->have_posts()) { + $query->the_post(); + if ($this->geocode_trainer(get_the_ID())) { + $geocoded++; + // Rate limiting - wait 100ms between requests + usleep(100000); + } + } + } + + wp_reset_postdata(); + + return $geocoded; + } + + /** + * AJAX handler for filtering trainers + */ + public function ajax_filter_trainers() { + check_ajax_referer('hvac_find_trainer', 'nonce'); + + $filters = $_POST['filters'] ?? []; + $page = isset($_POST['page']) ? intval($_POST['page']) : 1; + $per_page = 12; + + // Build query args + $args = $this->build_trainer_query_args($filters, $page, $per_page); + + $query = new WP_Query($args); + $trainers = []; + + if ($query->have_posts()) { + while ($query->have_posts()) { + $query->the_post(); + $trainers[] = $this->format_trainer_card_data(get_the_ID()); + } + } + + wp_reset_postdata(); + + // Sort trainers: Certified measureQuick Trainers first, Champions last + usort($trainers, function($a, $b) { + $a_cert = $a['certification']; + $b_cert = $b['certification']; + + // Define sort order: Trainers = 1, Champions = 2, Others = 3 + $a_priority = 3; // Default for others + $b_priority = 3; // Default for others + + if ($a_cert === 'Certified measureQuick Trainer') { + $a_priority = 1; + } elseif ($a_cert === 'Certified measureQuick Champion') { + $a_priority = 2; + } + + if ($b_cert === 'Certified measureQuick Trainer') { + $b_priority = 1; + } elseif ($b_cert === 'Certified measureQuick Champion') { + $b_priority = 2; + } + + // Primary sort by certification priority + if ($a_priority !== $b_priority) { + return $a_priority - $b_priority; + } + + // Secondary sort by name (alphabetical) + return strcasecmp($a['name'], $b['name']); + }); + + // Generate HTML for trainer cards + ob_start(); + if (!empty($trainers)) { + foreach ($trainers as $trainer) { + $this->render_trainer_card($trainer); + } + } else { + echo '

No trainers found matching your criteria. Try adjusting your filters.

'; + } + $html = ob_get_clean(); + + // Generate pagination HTML + $pagination_html = $this->generate_pagination($query->max_num_pages, $page); + + wp_send_json_success([ + 'html' => $html, + 'pagination' => $pagination_html, + 'count' => $query->found_posts, + 'page' => $page, + 'max_pages' => $query->max_num_pages + ]); + } + + /** + * AJAX handler for searching trainers + */ + public function ajax_search_trainers() { + check_ajax_referer('hvac_find_trainer', 'nonce'); + + $search = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : ''; + $page = isset($_POST['page']) ? intval($_POST['page']) : 1; + $per_page = 12; + + $filters = ['search' => $search]; + $args = $this->build_trainer_query_args($filters, $page, $per_page); + + $query = new WP_Query($args); + $trainers = []; + + if ($query->have_posts()) { + while ($query->have_posts()) { + $query->the_post(); + $trainers[] = $this->format_trainer_card_data(get_the_ID()); + } + } + + wp_reset_postdata(); + + // Sort trainers: Certified measureQuick Trainers first, Champions last + usort($trainers, function($a, $b) { + $a_cert = $a['certification']; + $b_cert = $b['certification']; + + // Define sort order: Trainers = 1, Champions = 2, Others = 3 + $a_priority = 3; // Default for others + $b_priority = 3; // Default for others + + if ($a_cert === 'Certified measureQuick Trainer') { + $a_priority = 1; + } elseif ($a_cert === 'Certified measureQuick Champion') { + $a_priority = 2; + } + + if ($b_cert === 'Certified measureQuick Trainer') { + $b_priority = 1; + } elseif ($b_cert === 'Certified measureQuick Champion') { + $b_priority = 2; + } + + // Primary sort by certification priority + if ($a_priority !== $b_priority) { + return $a_priority - $b_priority; + } + + // Secondary sort by name (alphabetical) + return strcasecmp($a['name'], $b['name']); + }); + + // Generate HTML + ob_start(); + foreach ($trainers as $trainer) { + $this->render_trainer_card($trainer); + } + $html = ob_get_clean(); + + $pagination_html = $this->generate_pagination($query->max_num_pages, $page); + + wp_send_json_success([ + 'html' => $html, + 'pagination' => $pagination_html, + 'count' => $query->found_posts + ]); + } + + /** + * Build query args for trainer search/filter + */ + private function build_trainer_query_args($filters, $page = 1, $per_page = 12) { + $args = [ + 'post_type' => 'trainer_profile', + 'posts_per_page' => $per_page, + 'paged' => $page, + 'post_status' => 'publish', + 'meta_query' => [ + 'relation' => 'AND', + [ + 'key' => 'is_public_profile', + 'value' => '1', + 'compare' => '=' + ] + ] + ]; + + // Add user status filter - only show profiles for approved/active/inactive users + $this->add_user_status_filter($args); + + // Apply search + if (!empty($filters['search'])) { + $args['meta_query'][] = [ + 'key' => 'trainer_display_name', + 'value' => $filters['search'], + 'compare' => 'LIKE' + ]; + } + + // Apply state filter - handle both single value and array + if (!empty($filters['state'])) { + if (is_array($filters['state'])) { + // Multiple states selected + $state_values = array_map('sanitize_text_field', $filters['state']); + $args['meta_query'][] = [ + 'key' => 'trainer_state', + 'value' => $state_values, + 'compare' => 'IN' + ]; + } else { + // Single state + $args['meta_query'][] = [ + 'key' => 'trainer_state', + 'value' => sanitize_text_field($filters['state']), + 'compare' => '=' + ]; + } + } + + // Apply taxonomy filters - handle arrays from multi-select + $tax_query = []; + + if (!empty($filters['business_type'])) { + $terms = is_array($filters['business_type']) ? array_map('sanitize_text_field', $filters['business_type']) : [sanitize_text_field($filters['business_type'])]; + $tax_query[] = [ + 'taxonomy' => 'business_type', + 'field' => 'name', // Changed from 'slug' to 'name' to match the values being sent + 'terms' => $terms + ]; + } + + if (!empty($filters['training_format'])) { + $terms = is_array($filters['training_format']) ? array_map('sanitize_text_field', $filters['training_format']) : [sanitize_text_field($filters['training_format'])]; + $tax_query[] = [ + 'taxonomy' => 'training_formats', + 'field' => 'name', // Changed from 'slug' to 'name' + 'terms' => $terms + ]; + } + + if (!empty($filters['training_resources'])) { + $terms = is_array($filters['training_resources']) ? array_map('sanitize_text_field', $filters['training_resources']) : [sanitize_text_field($filters['training_resources'])]; + $tax_query[] = [ + 'taxonomy' => 'training_resources', + 'field' => 'name', // Changed from 'slug' to 'name' + 'terms' => $terms + ]; + } + + if (!empty($tax_query)) { + $args['tax_query'] = $tax_query; + } + + return $args; + } + + /** + * Add user status filter to query args + * Only show profiles for users with status: approved, active, or inactive + * Exclude: pending, disabled + */ + private function add_user_status_filter(&$args) { + // Get all trainer profile user IDs first, then filter by status + $user_query = new WP_User_Query([ + 'meta_query' => [ + [ + 'key' => 'account_status', + 'value' => ['approved', 'active', 'inactive'], + 'compare' => 'IN' + ] + ], + 'fields' => 'ID' + ]); + + $approved_user_ids = $user_query->get_results(); + + if (!empty($approved_user_ids)) { + // Filter trainer profiles to only those belonging to approved users + $args['meta_query'][] = [ + 'key' => 'user_id', + 'value' => $approved_user_ids, + 'compare' => 'IN' + ]; + } else { + // If no approved users found, still show all profiles (fallback for development) + // In production, you might want to return no results instead + error_log('HVAC Debug: No users found with approved account status'); + } + } + + /** + * Format trainer data for card display + */ + private function format_trainer_card_data($profile_id) { + $user_id = get_post_meta($profile_id, 'user_id', true); + $trainer_name = get_post_meta($profile_id, 'trainer_display_name', true); + $city = get_post_meta($profile_id, 'trainer_city', true); + $state = get_post_meta($profile_id, 'trainer_state', true); + $certification = get_post_meta($profile_id, 'certification_type', true); + $profile_image = get_post_meta($profile_id, 'profile_image_url', true); + + // Get real event count for this trainer + $event_count = $this->get_trainer_event_count($user_id); + + return [ + 'profile_id' => $profile_id, + 'user_id' => $user_id, + 'name' => $trainer_name, + 'city' => $city, + 'state' => $state, + 'certification' => $certification, + 'profile_image' => $profile_image, + 'event_count' => $event_count, + 'profile_url' => '#' // Will open modal + ]; + } + + /** + * Render trainer card HTML + */ + private function render_trainer_card($trainer) { + ?> +
+
+
+ + <?php echo esc_attr($trainer['name']); ?> + +
+ +
+ + + + +
+ measureQuick Certified Trainer +
+ +
+
+

+ + + + + + + + + +

+

+ , +

+

+ +

+
+
+
+ '; + + // Previous + if ($current_page > 1) { + $html .= sprintf( + '%s', + $current_page - 1, + '«' + ); + } + + // Page numbers + for ($i = 1; $i <= $max_pages; $i++) { + if ($i == $current_page) { + $html .= sprintf('%d', $i); + } else { + $html .= sprintf( + '%d', + $i, + $i + ); + } + } + + // Next + if ($current_page < $max_pages) { + $html .= sprintf( + '%s', + $current_page + 1, + '»' + ); + } + + $html .= ''; + + return $html; + } + + /** + * Check for MapGeo plugin conflicts + */ + public function check_conflicts() { + $known_conflicts = [ + 'nextgen-gallery/nggallery.php', + 'wp-ulike/wp-ulike.php', + 'testimonials-widget/testimonials-widget.php', + 'wp-leaflet-maps-pro/wp-leaflet-maps-pro.php' + ]; + + $active_conflicts = []; + + foreach ($known_conflicts as $plugin) { + if (is_plugin_active($plugin)) { + $active_conflicts[] = $plugin; + } + } + + if (!empty($active_conflicts)) { + add_action('admin_notices', function() use ($active_conflicts) { + ?> +
+

+ HVAC Plugin Warning: + The following plugins may conflict with MapGeo integration: + +

+
+ init_hooks(); + } + + /** + * Initialize hooks + */ + private function init_hooks() { + // AJAX handlers + add_action('wp_ajax_hvac_get_filtered_trainers', [$this, 'ajax_get_filtered_trainers']); + add_action('wp_ajax_nopriv_hvac_get_filtered_trainers', [$this, 'ajax_get_filtered_trainers']); + + add_action('wp_ajax_hvac_get_filter_options', [$this, 'ajax_get_filter_options']); + add_action('wp_ajax_nopriv_hvac_get_filter_options', [$this, 'ajax_get_filter_options']); + + add_action('wp_ajax_hvac_get_trainer_profile', [$this, 'ajax_get_trainer_profile']); + add_action('wp_ajax_nopriv_hvac_get_trainer_profile', [$this, 'ajax_get_trainer_profile']); + + add_action('wp_ajax_hvac_search_trainers', [$this, 'ajax_search_trainers']); + add_action('wp_ajax_nopriv_hvac_search_trainers', [$this, 'ajax_search_trainers']); + + // Clear cache on profile updates + add_action('save_post_trainer_profile', [$this, 'clear_cache']); + add_action('deleted_post', [$this, 'clear_cache']); + } + + /** + * Get trainers based on filters + * + * @param array $args Query arguments + * @return array + */ + public function get_trainers($args = []) { + $defaults = [ + 'per_page' => 12, + 'page' => 1, + 'state' => '', + 'business_type' => '', + 'training_format' => '', + 'training_resources' => '', + 'search' => '', + 'orderby' => 'name', + 'order' => 'ASC' + ]; + + $args = wp_parse_args($args, $defaults); + + // Generate cache key + $cache_key = 'trainers_' . md5(serialize($args)); + $cached = wp_cache_get($cache_key, $this->cache_group); + + if (false !== $cached) { + return $cached; + } + + // Build query + $query_args = [ + 'post_type' => 'trainer_profile', + 'posts_per_page' => $args['per_page'], + 'paged' => $args['page'], + 'post_status' => 'publish', + 'meta_query' => [ + [ + 'key' => 'is_public_profile', + 'value' => '1', + 'compare' => '=' + ] + ] + ]; + + // Add meta queries + $meta_query = $this->build_meta_query($args); + if (!empty($meta_query)) { + $query_args['meta_query'] = array_merge($query_args['meta_query'], $meta_query); + } + + // Add taxonomy queries + $tax_query = $this->build_tax_query($args); + if (!empty($tax_query)) { + $query_args['tax_query'] = $tax_query; + } + + // Add search + if (!empty($args['search'])) { + $query_args['meta_query'][] = [ + 'relation' => 'OR', + [ + 'key' => 'trainer_display_name', + 'value' => $args['search'], + 'compare' => 'LIKE' + ], + [ + 'key' => 'trainer_city', + 'value' => $args['search'], + 'compare' => 'LIKE' + ], + [ + 'key' => 'company_name', + 'value' => $args['search'], + 'compare' => 'LIKE' + ] + ]; + } + + // Add ordering + $query_args = $this->add_ordering($query_args, $args['orderby'], $args['order']); + + // Execute query + $query = new WP_Query($query_args); + + $result = [ + 'trainers' => [], + 'total' => $query->found_posts, + 'pages' => $query->max_num_pages, + 'current_page' => $args['page'] + ]; + + if ($query->have_posts()) { + while ($query->have_posts()) { + $query->the_post(); + $result['trainers'][] = $this->format_trainer_data(get_the_ID()); + } + } + + wp_reset_postdata(); + + // Cache result + wp_cache_set($cache_key, $result, $this->cache_group, $this->cache_expiration); + + return $result; + } + + /** + * Build meta query from filters + * + * @param array $filters Filter values + * @return array + */ + private function build_meta_query($filters) { + $meta_query = []; + + if (!empty($filters['state'])) { + $meta_query[] = [ + 'key' => 'trainer_state', + 'value' => sanitize_text_field($filters['state']), + 'compare' => '=' + ]; + } + + return $meta_query; + } + + /** + * Build taxonomy query from filters + * + * @param array $filters Filter values + * @return array + */ + private function build_tax_query($filters) { + $tax_query = []; + + if (!empty($filters['business_type'])) { + $tax_query[] = [ + 'taxonomy' => 'business_type', + 'field' => 'slug', + 'terms' => sanitize_text_field($filters['business_type']) + ]; + } + + if (!empty($filters['training_format'])) { + $tax_query[] = [ + 'taxonomy' => 'training_formats', + 'field' => 'slug', + 'terms' => sanitize_text_field($filters['training_format']) + ]; + } + + if (!empty($filters['training_resources'])) { + $tax_query[] = [ + 'taxonomy' => 'training_resources', + 'field' => 'slug', + 'terms' => sanitize_text_field($filters['training_resources']) + ]; + } + + if (!empty($tax_query)) { + $tax_query['relation'] = 'AND'; + } + + return $tax_query; + } + + /** + * Add ordering to query + * + * @param array $query_args Query arguments + * @param string $orderby Order by field + * @param string $order Order direction + * @return array + */ + private function add_ordering($query_args, $orderby, $order) { + switch ($orderby) { + case 'name': + $query_args['meta_key'] = 'trainer_display_name'; + $query_args['orderby'] = 'meta_value'; + break; + + case 'city': + $query_args['meta_key'] = 'trainer_city'; + $query_args['orderby'] = 'meta_value'; + break; + + case 'state': + $query_args['meta_key'] = 'trainer_state'; + $query_args['orderby'] = 'meta_value'; + break; + + case 'events': + $query_args['orderby'] = 'meta_value_num'; + $query_args['meta_key'] = 'total_events_count'; + break; + + default: + $query_args['orderby'] = 'date'; + break; + } + + $query_args['order'] = in_array($order, ['ASC', 'DESC']) ? $order : 'ASC'; + + return $query_args; + } + + /** + * Format trainer data for response + * + * @param int $profile_id Trainer profile post ID + * @return array + */ + private function format_trainer_data($profile_id) { + $user_id = get_post_meta($profile_id, 'user_id', true); + + $data = [ + 'profile_id' => $profile_id, + 'user_id' => $user_id, + 'name' => get_post_meta($profile_id, 'trainer_display_name', true), + 'city' => get_post_meta($profile_id, 'trainer_city', true), + 'state' => get_post_meta($profile_id, 'trainer_state', true), + 'country' => get_post_meta($profile_id, 'trainer_country', true) ?: 'USA', + 'certification_type' => get_post_meta($profile_id, 'certification_type', true), + 'certification_status' => get_post_meta($profile_id, 'certification_status', true), + 'profile_image' => get_post_meta($profile_id, 'profile_image_url', true), + 'company_name' => get_post_meta($profile_id, 'company_name', true), + 'company_website' => get_post_meta($profile_id, 'company_website', true), + 'phone' => get_post_meta($profile_id, 'trainer_phone', true), + 'email' => get_post_meta($profile_id, 'trainer_email', true), + 'bio' => get_post_meta($profile_id, 'trainer_bio', true), + 'latitude' => get_post_meta($profile_id, 'latitude', true), + 'longitude' => get_post_meta($profile_id, 'longitude', true) + ]; + + // Get taxonomies + $data['business_type'] = wp_get_post_terms($profile_id, 'business_type', ['fields' => 'names']); + $data['training_formats'] = wp_get_post_terms($profile_id, 'training_formats', ['fields' => 'names']); + $data['training_locations'] = wp_get_post_terms($profile_id, 'training_locations', ['fields' => 'names']); + $data['training_resources'] = wp_get_post_terms($profile_id, 'training_resources', ['fields' => 'names']); + $data['training_audience'] = wp_get_post_terms($profile_id, 'training_audience', ['fields' => 'names']); + + // Get upcoming events + $data['upcoming_events'] = $this->get_trainer_events($user_id); + $data['total_events'] = get_post_meta($profile_id, 'total_events_count', true) ?: 0; + + return $data; + } + + /** + * Get trainer's upcoming events + * + * @param int $user_id Trainer user ID + * @return array + */ + private function get_trainer_events($user_id, $limit = 5) { + if (!function_exists('tribe_get_events')) { + return []; + } + + $events = tribe_get_events([ + 'author' => $user_id, + 'eventDisplay' => 'upcoming', + 'posts_per_page' => $limit + ]); + + $formatted_events = []; + + foreach ($events as $event) { + $formatted_events[] = [ + 'id' => $event->ID, + 'title' => $event->post_title, + 'start_date' => tribe_get_start_date($event->ID, false, 'Y-m-d'), + 'start_time' => tribe_get_start_time($event->ID), + 'end_date' => tribe_get_end_date($event->ID, false, 'Y-m-d'), + 'venue' => tribe_get_venue($event->ID), + 'city' => tribe_get_city($event->ID), + 'state' => tribe_get_state($event->ID), + 'url' => get_permalink($event->ID) + ]; + } + + return $formatted_events; + } + + /** + * Get filter options for a taxonomy + * + * @param string $taxonomy Taxonomy name + * @return array + */ + public function get_filter_options($taxonomy) { + $valid_taxonomies = [ + 'business_type', + 'training_formats', + 'training_locations', + 'training_resources', + 'training_audience' + ]; + + if (!in_array($taxonomy, $valid_taxonomies)) { + return []; + } + + $terms = get_terms([ + 'taxonomy' => $taxonomy, + 'hide_empty' => true, + 'orderby' => 'name', + 'order' => 'ASC' + ]); + + $options = []; + + if (!is_wp_error($terms)) { + foreach ($terms as $term) { + $options[] = [ + 'value' => $term->slug, + 'label' => $term->name, + 'count' => $term->count + ]; + } + } + + return $options; + } + + /** + * Get state/province options + * + * @return array + */ + public function get_state_options() { + global $wpdb; + + $table = $wpdb->postmeta; + $states = $wpdb->get_col( + "SELECT DISTINCT meta_value + FROM $table + WHERE meta_key = 'trainer_state' + AND meta_value != '' + ORDER BY meta_value ASC" + ); + + $options = []; + + foreach ($states as $state) { + $options[] = [ + 'value' => $state, + 'label' => $state + ]; + } + + return $options; + } + + /** + * AJAX handler for getting filtered trainers + */ + public function ajax_get_filtered_trainers() { + check_ajax_referer('hvac_find_trainer', 'nonce'); + + $filters = $_POST['filters'] ?? []; + $page = intval($_POST['page'] ?? 1); + $per_page = intval($_POST['per_page'] ?? 12); + + $args = [ + 'page' => $page, + 'per_page' => $per_page, + 'state' => sanitize_text_field($filters['state'] ?? ''), + 'business_type' => sanitize_text_field($filters['business_type'] ?? ''), + 'training_format' => sanitize_text_field($filters['training_format'] ?? ''), + 'training_resources' => sanitize_text_field($filters['training_resources'] ?? ''), + 'search' => sanitize_text_field($filters['search'] ?? '') + ]; + + $result = $this->get_trainers($args); + + // Generate HTML for directory + ob_start(); + if (!empty($result['trainers'])) { + foreach ($result['trainers'] as $trainer) { + $this->render_trainer_card($trainer); + } + } else { + echo '

No trainers found matching your criteria.

'; + } + $html = ob_get_clean(); + + wp_send_json_success([ + 'trainers' => $result['trainers'], + 'html' => $html, + 'total' => $result['total'], + 'pages' => $result['pages'], + 'current_page' => $result['current_page'] + ]); + } + + /** + * AJAX handler for getting filter options + */ + public function ajax_get_filter_options() { + check_ajax_referer('hvac_find_trainer', 'nonce'); + + $filter_type = sanitize_text_field($_POST['filter_type'] ?? ''); + + if ($filter_type === 'state') { + $options = $this->get_state_options(); + } else { + $options = $this->get_filter_options($filter_type); + } + + wp_send_json_success(['options' => $options]); + } + + /** + * AJAX handler for getting trainer profile + */ + public function ajax_get_trainer_profile() { + check_ajax_referer('hvac_find_trainer', 'nonce'); + + $profile_id = intval($_POST['profile_id'] ?? 0); + + if (!$profile_id) { + wp_send_json_error(['message' => 'Invalid trainer profile']); + } + + $trainer_data = $this->format_trainer_data($profile_id); + + if (empty($trainer_data['name'])) { + wp_send_json_error(['message' => 'Trainer not found']); + } + + // Generate profile HTML + ob_start(); + $this->render_trainer_profile_modal($trainer_data); + $html = ob_get_clean(); + + wp_send_json_success([ + 'trainer' => $trainer_data, + 'html' => $html + ]); + } + + /** + * AJAX handler for searching trainers + */ + public function ajax_search_trainers() { + check_ajax_referer('hvac_find_trainer', 'nonce'); + + $search = sanitize_text_field($_POST['search'] ?? ''); + + if (strlen($search) < 2) { + wp_send_json_error(['message' => 'Search term too short']); + } + + $result = $this->get_trainers(['search' => $search, 'per_page' => 20]); + + wp_send_json_success([ + 'trainers' => $result['trainers'], + 'total' => $result['total'] + ]); + } + + /** + * Render trainer card + * + * @param array $trainer Trainer data + */ + private function render_trainer_card($trainer) { + ?> +
+
+
+ + <?php echo esc_attr($trainer['name']); ?> + +
+ +
+ +
+ +
+

+ + + +

+

+ , +

+ +

+ +

+ +
+ +
+
+
+
+ +
+

+ +
+ +
+
+
+ + <?php echo esc_attr($trainer['name']); ?> + +
+ +
+ + +
+

+ , +

+ + +

+ +

+ + + +

+ +

+ + +

+ Total Training Events: +

+
+
+ + +
+ Training Formats: +
+ + + +
+ Training Locations: +
+ + + +
+

Upcoming Events:

+
    + +
  • + + + + - + + () + +
  • + +
+
+ +
+ +
+

Contact

+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + + + +
+
+ cache_group); + } else { + // Fallback for older WordPress versions + wp_cache_flush(); + } + } +} \ No newline at end of file diff --git a/includes/google-sheets/class-google-sheets-admin.php b/includes/google-sheets/class-google-sheets-admin.php new file mode 100644 index 00000000..b15dd6a6 --- /dev/null +++ b/includes/google-sheets/class-google-sheets-admin.php @@ -0,0 +1,540 @@ +auth = new HVAC_Google_Sheets_Auth(); + $this->manager = new HVAC_Google_Sheets_Manager(); + + add_action('wp_ajax_hvac_create_master_report', array($this, 'ajax_create_master_report')); + add_action('wp_ajax_hvac_create_event_spreadsheet', array($this, 'ajax_create_event_spreadsheet')); + add_action('wp_ajax_hvac_test_google_sheets_connection', array($this, 'ajax_test_connection')); + add_action('wp_ajax_hvac_verify_folder_structure', array($this, 'ajax_verify_folder_structure')); + } + + /** + * Render Google Sheets admin page + */ + public function render_admin_page() { + try { + // Initialize with safe defaults + $auth_status = array( + 'has_credentials' => false, + 'is_authenticated' => false, + 'client_id' => 'Not configured', + 'token_expires' => 'N/A' + ); + + // Try to get auth status + if ($this->auth) { + $auth_status = $this->auth->get_config_status(); + } + + // Initialize report variables + $latest_report = null; + $report_history = array(); + + // Try to get reports if manager is available + if ($this->manager) { + $latest_report = $this->manager->get_latest_master_report(); + $report_history = $this->manager->get_master_report_history(); + } + + ?> +
+
+
+

Google Sheets Integration

+ +
+ + + +
+ + Success! Google Sheets authorization completed successfully! You can now create reports. +
+ + + +
+ + Error: +
+ + + +
+
+

Connection Status

+
+
+
+
+ Credentials: + + + +
+
+ Authentication: + + + +
+
+ Client ID: + +
+
+ Token Expires: + +
+
+ +
+ + + + + Authorize Access + + +
+
+
+ + +
+
+

Master Report

+
+
+

Generate a comprehensive report with system overview, trainer performance, all events, and revenue analytics.

+ + +
+

Latest Report

+
+ +
+
+ + +
+ +
+
+
+ + +
+
+

Event Spreadsheets

+
+
+

Create detailed spreadsheets for individual events with attendees, financial data, and event details.

+ +
+ + + +
+ +
+

Existing Event Spreadsheets

+
+ render_existing_event_sheets(); ?> +
+
+
+
+ + + +
+
+

Report History

+
+
+
+ +
+
+ + + + + by display_name; ?> + +
+ + Open + +
+ +
+
+
+ +
+
+ + + + + + +
+
+
+ + Error: Unable to load Google Sheets integration. getMessage()); ?> +
+ +
+
+ getMessage()); + } + } + + /** + * Render event options for select dropdown + */ + private function render_event_options() { + $events = get_posts(array( + 'post_type' => 'tribe_events', + 'post_status' => 'publish', + 'numberposts' => -1, + 'orderby' => 'meta_value', + 'meta_key' => '_EventStartDate', + 'order' => 'DESC' + )); + + foreach ($events as $event) { + $event_date = get_post_meta($event->ID, '_EventStartDate', true); + $formatted_date = $event_date ? date('M j, Y', strtotime($event_date)) : 'No date'; + $trainer_name = get_the_author_meta('display_name', $event->post_author); + + echo ''; + } + } + + /** + * Render existing event spreadsheets + */ + private function render_existing_event_sheets() { + global $wpdb; + + $results = $wpdb->get_results( + "SELECT p.ID, p.post_title, pm.meta_value, u.display_name + FROM {$wpdb->posts} p + JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + JOIN {$wpdb->users} u ON p.post_author = u.ID + WHERE p.post_type = 'tribe_events' + AND pm.meta_key = '_hvac_google_sheet' + ORDER BY p.post_date DESC" + ); + + if (empty($results)) { + echo '

No event spreadsheets created yet.

'; + return; + } + + foreach ($results as $result) { + $sheet_data = maybe_unserialize($result->meta_value); + if (is_array($sheet_data) && isset($sheet_data['url'])) { + echo '
'; + echo '
'; + echo '

' . esc_html($result->post_title) . '

'; + echo 'by ' . esc_html($result->display_name) . ''; + echo 'Created: ' . date('M j, Y', strtotime($sheet_data['created_at'])) . ''; + echo '
'; + echo ''; + echo ' Open Spreadsheet'; + echo ''; + echo '
'; + } + } + } + + /** + * AJAX: Create Master Report + */ + public function ajax_create_master_report() { + check_ajax_referer('hvac_google_sheets', '_wpnonce'); + + if (!current_user_can('view_master_dashboard')) { + wp_die('Insufficient permissions'); + } + + $result = $this->manager->create_master_report(); + + if ($result['success']) { + wp_send_json_success($result); + } else { + wp_send_json_error($result['error']); + } + } + + /** + * AJAX: Create Event Spreadsheet + */ + public function ajax_create_event_spreadsheet() { + check_ajax_referer('hvac_google_sheets', '_wpnonce'); + + if (!current_user_can('view_master_dashboard')) { + wp_die('Insufficient permissions'); + } + + $event_id = intval($_POST['event_id']); + if (!$event_id) { + wp_send_json_error('Invalid event ID'); + } + + $result = $this->manager->create_event_spreadsheet($event_id); + + if ($result['success']) { + wp_send_json_success($result); + } else { + wp_send_json_error($result['error']); + } + } + + /** + * AJAX: Test Connection + */ + public function ajax_test_connection() { + check_ajax_referer('hvac_google_sheets', '_wpnonce'); + + if (!current_user_can('view_master_dashboard')) { + wp_die('Insufficient permissions'); + } + + $result = $this->manager->test_connection(); + + if ($result['success']) { + wp_send_json_success($result['message']); + } else { + wp_send_json_error($result['message']); + } + } + + /** + * AJAX: Verify Folder Structure + */ + public function ajax_verify_folder_structure() { + check_ajax_referer('hvac_google_sheets', '_wpnonce'); + + if (!current_user_can('view_master_dashboard')) { + wp_die('Insufficient permissions'); + } + + require_once plugin_dir_path(dirname(__FILE__)) . '../google-sheets-folder-manager.php'; + $folder_manager = new HVAC_Google_Sheets_Folder_Manager(); + + $result = $folder_manager->verify_folder_structure(); + + wp_send_json_success($result); + } +} \ No newline at end of file diff --git a/includes/google-sheets/class-google-sheets-auth.php b/includes/google-sheets/class-google-sheets-auth.php new file mode 100644 index 00000000..e19e692b --- /dev/null +++ b/includes/google-sheets/class-google-sheets-auth.php @@ -0,0 +1,435 @@ +client_id = defined('GOOGLE_SHEETS_CLIENT_ID') ? GOOGLE_SHEETS_CLIENT_ID : ''; + $this->client_secret = defined('GOOGLE_SHEETS_CLIENT_SECRET') ? GOOGLE_SHEETS_CLIENT_SECRET : ''; + $this->refresh_token = defined('GOOGLE_SHEETS_REFRESH_TOKEN') ? GOOGLE_SHEETS_REFRESH_TOKEN : ''; + $this->redirect_uri = defined('GOOGLE_SHEETS_REDIRECT_URI') ? GOOGLE_SHEETS_REDIRECT_URI : 'http://localhost:8080/callback'; + $this->folder_id = defined('GOOGLE_SHEETS_FOLDER_ID') ? GOOGLE_SHEETS_FOLDER_ID : ''; + } + + // Load stored access token from WordPress options + $this->load_access_token(); + + // Register callback handler - use template_redirect to catch it before page rendering + add_action('template_redirect', array($this, 'handle_oauth_callback')); + } + + /** + * Generate authorization URL for initial setup + */ + public function get_authorization_url() { + $params = array( + 'client_id' => $this->client_id, + 'redirect_uri' => $this->redirect_uri, + 'scope' => 'https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.file', + 'response_type' => 'code', + 'access_type' => 'offline', + 'prompt' => 'consent', + 'include_granted_scopes' => 'true' + ); + + return $this->auth_url . '?' . http_build_query($params); + } + + /** + * Exchange authorization code for tokens + */ + public function exchange_code_for_tokens($auth_code) { + $params = array( + 'client_id' => $this->client_id, + 'client_secret' => $this->client_secret, + 'redirect_uri' => $this->redirect_uri, + 'grant_type' => 'authorization_code', + 'code' => $auth_code + ); + + if (class_exists('HVAC_Logger')) { + HVAC_Logger::info("Token exchange request params: " . json_encode(array( + 'client_id' => substr($this->client_id, 0, 20) . '...', + 'redirect_uri' => $this->redirect_uri, + 'grant_type' => 'authorization_code', + 'code' => substr($auth_code, 0, 20) . '...' + )), 'GoogleSheets'); + } + + $response = wp_remote_post($this->token_url, array( + 'body' => $params, + 'headers' => array( + 'Content-Type' => 'application/x-www-form-urlencoded' + ), + 'timeout' => 30 + )); + + if (is_wp_error($response)) { + $this->log_error('Failed to exchange code: ' . $response->get_error_message()); + return false; + } + + $response_code = wp_remote_retrieve_response_code($response); + $body = wp_remote_retrieve_body($response); + + if (class_exists('HVAC_Logger')) { + HVAC_Logger::info("Token exchange response code: " . $response_code, 'GoogleSheets'); + HVAC_Logger::info("Token exchange response body: " . $body, 'GoogleSheets'); + } + + $data = json_decode($body, true); + + if (isset($data['access_token'])) { + $this->access_token = $data['access_token']; + if (isset($data['refresh_token'])) { + $this->refresh_token = $data['refresh_token']; + } + $this->token_expiry = time() + (int)$data['expires_in']; + + if (class_exists('HVAC_Logger')) { + HVAC_Logger::info("Successfully received tokens. Access token: " . substr($this->access_token, 0, 20) . "...", 'GoogleSheets'); + HVAC_Logger::info("Refresh token: " . ($this->refresh_token ? 'RECEIVED' : 'NOT RECEIVED'), 'GoogleSheets'); + HVAC_Logger::info("Token expires at: " . date('Y-m-d H:i:s', $this->token_expiry), 'GoogleSheets'); + } + + // Save tokens + $this->save_tokens(); + + return true; + } + + $this->log_error('Invalid token response (status ' . $response_code . '): ' . $body); + return false; + } + + /** + * Get valid access token (refresh if needed) + */ + public function get_access_token() { + // Check if token is expired or will expire in next 5 minutes + if ($this->token_expiry && ($this->token_expiry - 300) < time()) { + $this->refresh_access_token(); + } + + return $this->access_token; + } + + /** + * Refresh access token using refresh token + */ + private function refresh_access_token() { + if (empty($this->refresh_token)) { + $this->log_error('No refresh token available'); + return false; + } + + $params = array( + 'client_id' => $this->client_id, + 'client_secret' => $this->client_secret, + 'refresh_token' => $this->refresh_token, + 'grant_type' => 'refresh_token' + ); + + $response = wp_remote_post($this->token_url, array( + 'body' => $params, + 'headers' => array( + 'Content-Type' => 'application/x-www-form-urlencoded' + ) + )); + + if (is_wp_error($response)) { + $this->log_error('Failed to refresh token: ' . $response->get_error_message()); + return false; + } + + $body = wp_remote_retrieve_body($response); + $data = json_decode($body, true); + + if (isset($data['access_token'])) { + $this->access_token = $data['access_token']; + $this->token_expiry = time() + $data['expires_in']; + + // Update refresh token if provided + if (isset($data['refresh_token'])) { + $this->refresh_token = $data['refresh_token']; + } + + // Save updated tokens + $this->save_tokens(); + + return true; + } + + $this->log_error('Failed to refresh token: ' . $body); + return false; + } + + /** + * Make authenticated API request to Google Sheets/Drive + */ + public function make_api_request($method, $endpoint, $data = null, $api_type = 'sheets') { + $access_token = $this->get_access_token(); + if (!$access_token) { + throw new Exception('No valid access token available'); + } + + $base_url = ($api_type === 'drive') ? $this->drive_api_url : $this->sheets_api_url; + $url = $base_url . $endpoint; + + // Handle valueInputOption as query parameter for Sheets API + if ($data && isset($data['valueInputOption'])) { + $url .= '?valueInputOption=' . urlencode($data['valueInputOption']); + unset($data['valueInputOption']); // Remove from body data + } + + $args = array( + 'method' => $method, + 'headers' => array( + 'Authorization' => 'Bearer ' . $access_token, + 'Content-Type' => 'application/json' + ), + 'timeout' => 30 + ); + + if ($data && in_array($method, ['POST', 'PUT', 'PATCH'])) { + $args['body'] = json_encode($data); + } + + $response = wp_remote_request($url, $args); + + if (is_wp_error($response)) { + throw new Exception('API request failed: ' . $response->get_error_message()); + } + + $response_code = wp_remote_retrieve_response_code($response); + $body = wp_remote_retrieve_body($response); + + if ($response_code >= 400) { + $error_data = json_decode($body, true); + $error_message = isset($error_data['error']['message']) ? $error_data['error']['message'] : 'Unknown API error'; + throw new Exception("API error {$response_code}: {$error_message}"); + } + + return json_decode($body, true); + } + + /** + * Test API connection + */ + public function test_connection() { + try { + // Try to create a test spreadsheet in the designated folder + $spreadsheet_data = array( + 'properties' => array( + 'title' => 'HVAC Test Connection - ' . date('Y-m-d H:i:s') + ) + ); + + $response = $this->make_api_request('POST', '', $spreadsheet_data); + + if (isset($response['spreadsheetId'])) { + // Move to designated folder if specified + if ($this->folder_id) { + $this->make_api_request( + 'PATCH', + '/' . $response['spreadsheetId'] . '?addParents=' . $this->folder_id, + null, + 'drive' + ); + } + + // Delete test spreadsheet + $this->make_api_request( + 'DELETE', + '/' . $response['spreadsheetId'], + null, + 'drive' + ); + + return array('success' => true, 'message' => 'Connection successful'); + } + + return array('success' => false, 'message' => 'Unexpected response format'); + + } catch (Exception $e) { + return array('success' => false, 'message' => $e->getMessage()); + } + } + + /** + * Load access token from WordPress options + */ + private function load_access_token() { + $token_data = get_option('hvac_google_sheets_tokens', array()); + + if (!empty($token_data)) { + $this->access_token = $token_data['access_token'] ?? ''; + $this->refresh_token = $token_data['refresh_token'] ?? $this->refresh_token; + $this->token_expiry = $token_data['expires_at'] ?? 0; + } + } + + /** + * Save tokens to WordPress options + */ + private function save_tokens() { + $token_data = array( + 'access_token' => $this->access_token, + 'refresh_token' => $this->refresh_token, + 'expires_at' => $this->token_expiry, + 'created_at' => time() + ); + + $result = update_option('hvac_google_sheets_tokens', $token_data); + + if (class_exists('HVAC_Logger')) { + HVAC_Logger::info("Saving tokens to database: " . ($result ? 'SUCCESS' : 'FAILED'), 'GoogleSheets'); + HVAC_Logger::info("Token data: " . json_encode(array( + 'access_token' => substr($this->access_token, 0, 20) . '...', + 'refresh_token' => $this->refresh_token ? 'SET' : 'NOT SET', + 'expires_at' => date('Y-m-d H:i:s', $this->token_expiry), + 'created_at' => date('Y-m-d H:i:s', time()) + )), 'GoogleSheets'); + } + } + + /** + * Clear stored tokens + */ + public function clear_tokens() { + delete_option('hvac_google_sheets_tokens'); + $this->access_token = ''; + $this->refresh_token = ''; + $this->token_expiry = 0; + } + + /** + * Check if we have valid credentials + */ + public function has_valid_credentials() { + return !empty($this->client_id) && !empty($this->client_secret); + } + + /** + * Check if we have an access token + */ + public function is_authenticated() { + return !empty($this->access_token) || !empty($this->refresh_token); + } + + /** + * Get last error message + */ + public function get_last_error() { + return $this->last_error; + } + + /** + * Log error message + */ + private function log_error($message) { + $this->last_error = $message; + if (class_exists('HVAC_Logger')) { + HVAC_Logger::error("Google Sheets Auth: {$message}", 'GoogleSheets'); + } + error_log("HVAC Google Sheets Auth Error: {$message}"); + } + + /** + * Get configuration status + */ + public function get_config_status() { + return array( + 'has_credentials' => $this->has_valid_credentials(), + 'is_authenticated' => $this->is_authenticated(), + 'client_id' => !empty($this->client_id) ? substr($this->client_id, 0, 10) . '...' : '', + 'has_refresh_token' => !empty($this->refresh_token), + 'token_expires' => $this->token_expiry ? date('Y-m-d H:i:s', $this->token_expiry) : 'Unknown', + 'folder_id' => $this->folder_id + ); + } + + /** + * Handle OAuth callback from Google + */ + public function handle_oauth_callback() { + // Debug: Log all OAuth callback attempts + if (isset($_GET['code']) && isset($_GET['scope'])) { + if (class_exists('HVAC_Logger')) { + HVAC_Logger::info("OAuth callback detected - URI: " . $_SERVER['REQUEST_URI'], 'GoogleSheets'); + HVAC_Logger::info("OAuth callback - code param: " . substr($_GET['code'], 0, 20) . "...", 'GoogleSheets'); + } + error_log("HVAC Google OAuth callback detected - URI: " . $_SERVER['REQUEST_URI']); + error_log("HVAC Google OAuth callback - code: " . substr($_GET['code'], 0, 20) . "..."); + } + + // Check if this is an OAuth callback request to the Google Sheets page + if (isset($_GET['code']) && isset($_GET['scope']) && + (strpos($_SERVER['REQUEST_URI'], '/google-sheets/') !== false || + strpos($_SERVER['REQUEST_URI'], 'google-sheets') !== false)) { + + $auth_code = sanitize_text_field($_GET['code']); + + if (class_exists('HVAC_Logger')) { + HVAC_Logger::info("Processing OAuth callback with code: " . substr($auth_code, 0, 20) . "...", 'GoogleSheets'); + HVAC_Logger::info("Current redirect URI: " . $this->redirect_uri, 'GoogleSheets'); + } + + // Exchange the authorization code for tokens + $success = $this->exchange_code_for_tokens($auth_code); + + if (class_exists('HVAC_Logger')) { + HVAC_Logger::info("Token exchange result: " . ($success ? 'SUCCESS' : 'FAILED'), 'GoogleSheets'); + if (!$success) { + HVAC_Logger::error("Token exchange error: " . $this->get_last_error(), 'GoogleSheets'); + } + } + + if ($success) { + // Redirect to Google Sheets admin page with success message (clean URL) + wp_redirect(add_query_arg(array( + 'auth_success' => '1' + ), home_url('/google-sheets/'))); + exit; + } else { + // Redirect to Google Sheets admin page with error message + wp_redirect(add_query_arg(array( + 'auth_error' => '1', + 'message' => urlencode($this->get_last_error()) + ), home_url('/google-sheets/'))); + exit; + } + } + } +} \ No newline at end of file diff --git a/includes/google-sheets/class-google-sheets-folder-manager.php b/includes/google-sheets/class-google-sheets-folder-manager.php new file mode 100644 index 00000000..dbc6685d --- /dev/null +++ b/includes/google-sheets/class-google-sheets-folder-manager.php @@ -0,0 +1,390 @@ +auth = new HVAC_Google_Sheets_Auth(); + + if (class_exists('HVAC_Logger')) { + $this->logger = new HVAC_Logger(); + } + } + + /** + * Get or create the root "Upskill Training Sheets" folder + */ + public function get_root_folder_id() { + if ($this->root_folder_id) { + return $this->root_folder_id; + } + + try { + // First, search for existing folder + $existing_folder = $this->find_folder_by_name(self::ROOT_FOLDER_NAME); + + if ($existing_folder) { + $this->root_folder_id = $existing_folder['id']; + $this->log_info("Found existing root folder: {$this->root_folder_id}"); + + // Ensure proper permissions are set + $this->set_organization_permissions($this->root_folder_id); + + return $this->root_folder_id; + } + + // Create new root folder + $folder_data = array( + 'name' => self::ROOT_FOLDER_NAME, + 'mimeType' => 'application/vnd.google-apps.folder' + ); + + $response = $this->auth->make_drive_api_request('POST', 'files', $folder_data); + + if (isset($response['id'])) { + $this->root_folder_id = $response['id']; + $this->log_info("Created root folder: {$this->root_folder_id}"); + + // Set organization permissions + $this->set_organization_permissions($this->root_folder_id); + + // Make discoverable in search + $this->make_folder_discoverable($this->root_folder_id); + + return $this->root_folder_id; + } + + throw new Exception('Failed to create root folder'); + + } catch (Exception $e) { + $this->log_error('Failed to get/create root folder: ' . $e->getMessage()); + return false; + } + } + + /** + * Get or create the "_Master Trainer" folder + */ + public function get_master_trainer_folder_id() { + if ($this->master_folder_id) { + return $this->master_folder_id; + } + + $root_folder_id = $this->get_root_folder_id(); + if (!$root_folder_id) { + return false; + } + + try { + // Search for existing master trainer folder + $existing_folder = $this->find_folder_by_name(self::MASTER_TRAINER_FOLDER_NAME, $root_folder_id); + + if ($existing_folder) { + $this->master_folder_id = $existing_folder['id']; + $this->log_info("Found existing master trainer folder: {$this->master_folder_id}"); + return $this->master_folder_id; + } + + // Create master trainer folder + $folder_data = array( + 'name' => self::MASTER_TRAINER_FOLDER_NAME, + 'mimeType' => 'application/vnd.google-apps.folder', + 'parents' => array($root_folder_id) + ); + + $response = $this->auth->make_drive_api_request('POST', 'files', $folder_data); + + if (isset($response['id'])) { + $this->master_folder_id = $response['id']; + $this->log_info("Created master trainer folder: {$this->master_folder_id}"); + return $this->master_folder_id; + } + + throw new Exception('Failed to create master trainer folder'); + + } catch (Exception $e) { + $this->log_error('Failed to get/create master trainer folder: ' . $e->getMessage()); + return false; + } + } + + /** + * Get or create an event-specific folder + */ + public function get_event_folder_id($event_id) { + if (isset($this->event_folders[$event_id])) { + return $this->event_folders[$event_id]; + } + + $root_folder_id = $this->get_root_folder_id(); + if (!$root_folder_id) { + return false; + } + + $event = get_post($event_id); + if (!$event) { + $this->log_error("Event not found: {$event_id}"); + return false; + } + + $folder_name = self::EVENT_FOLDER_PREFIX . $event->post_title; + + try { + // Search for existing event folder + $existing_folder = $this->find_folder_by_name($folder_name, $root_folder_id); + + if ($existing_folder) { + $this->event_folders[$event_id] = $existing_folder['id']; + $this->log_info("Found existing event folder for {$event_id}: {$existing_folder['id']}"); + return $this->event_folders[$event_id]; + } + + // Create event folder + $folder_data = array( + 'name' => $folder_name, + 'mimeType' => 'application/vnd.google-apps.folder', + 'parents' => array($root_folder_id) + ); + + $response = $this->auth->make_drive_api_request('POST', 'files', $folder_data); + + if (isset($response['id'])) { + $this->event_folders[$event_id] = $response['id']; + $this->log_info("Created event folder for {$event_id}: {$response['id']}"); + return $this->event_folders[$event_id]; + } + + throw new Exception('Failed to create event folder'); + + } catch (Exception $e) { + $this->log_error("Failed to get/create event folder for {$event_id}: " . $e->getMessage()); + return false; + } + } + + /** + * Set organization-wide permissions on a folder + */ + private function set_organization_permissions($folder_id) { + try { + // Set permissions for measureQuick.com organization + $permission_data = array( + 'role' => 'writer', + 'type' => 'domain', + 'domain' => 'measurequick.com', + 'allowFileDiscovery' => true + ); + + $response = $this->auth->make_drive_api_request('POST', "files/{$folder_id}/permissions", $permission_data); + + if (isset($response['id'])) { + $this->log_info("Set organization permissions on folder: {$folder_id}"); + return true; + } + + throw new Exception('Failed to set permissions'); + + } catch (Exception $e) { + $this->log_error("Failed to set organization permissions on {$folder_id}: " . $e->getMessage()); + return false; + } + } + + /** + * Make folder discoverable in Google Search + */ + private function make_folder_discoverable($folder_id) { + try { + // Update folder to be discoverable + $folder_data = array( + 'capabilities' => array( + 'canAddChildren' => true, + 'canListChildren' => true, + 'canRemoveChildren' => true + ), + 'viewersCanCopyContent' => true, + 'copyRequiresWriterPermission' => false + ); + + $response = $this->auth->make_drive_api_request('PATCH', "files/{$folder_id}", $folder_data); + + if (isset($response['id'])) { + $this->log_info("Made folder discoverable: {$folder_id}"); + return true; + } + + throw new Exception('Failed to make folder discoverable'); + + } catch (Exception $e) { + $this->log_error("Failed to make folder discoverable {$folder_id}: " . $e->getMessage()); + return false; + } + } + + /** + * Find a folder by name, optionally within a parent folder + */ + private function find_folder_by_name($name, $parent_id = null) { + try { + $query = "name='{$name}' and mimeType='application/vnd.google-apps.folder' and trashed=false"; + + if ($parent_id) { + $query .= " and '{$parent_id}' in parents"; + } + + $response = $this->auth->make_drive_api_request('GET', 'files', null, array( + 'q' => $query, + 'fields' => 'files(id,name,parents)', + 'pageSize' => 10 + )); + + if (isset($response['files']) && count($response['files']) > 0) { + return $response['files'][0]; // Return first match + } + + return null; + + } catch (Exception $e) { + $this->log_error("Failed to search for folder '{$name}': " . $e->getMessage()); + return null; + } + } + + /** + * Get folder structure overview + */ + public function get_folder_structure() { + $structure = array( + 'root' => array( + 'name' => self::ROOT_FOLDER_NAME, + 'id' => $this->get_root_folder_id(), + 'url' => null + ), + 'master_trainer' => array( + 'name' => self::MASTER_TRAINER_FOLDER_NAME, + 'id' => $this->get_master_trainer_folder_id(), + 'url' => null + ), + 'event_folders' => array() + ); + + // Add URLs for existing folders + if ($structure['root']['id']) { + $structure['root']['url'] = "https://drive.google.com/drive/folders/{$structure['root']['id']}"; + } + + if ($structure['master_trainer']['id']) { + $structure['master_trainer']['url'] = "https://drive.google.com/drive/folders/{$structure['master_trainer']['id']}"; + } + + return $structure; + } + + /** + * Verify and repair folder structure + */ + public function verify_folder_structure() { + $results = array(); + + // Check root folder + $root_id = $this->get_root_folder_id(); + $results['root_folder'] = array( + 'status' => $root_id ? 'exists' : 'missing', + 'id' => $root_id, + 'message' => $root_id ? 'Root folder found/created successfully' : 'Failed to create root folder' + ); + + // Check master trainer folder + if ($root_id) { + $master_id = $this->get_master_trainer_folder_id(); + $results['master_trainer_folder'] = array( + 'status' => $master_id ? 'exists' : 'missing', + 'id' => $master_id, + 'message' => $master_id ? 'Master trainer folder found/created successfully' : 'Failed to create master trainer folder' + ); + } + + // Check permissions + if ($root_id) { + $permissions_ok = $this->verify_organization_permissions($root_id); + $results['permissions'] = array( + 'status' => $permissions_ok ? 'configured' : 'missing', + 'message' => $permissions_ok ? 'Organization permissions configured' : 'Failed to configure organization permissions' + ); + } + + return $results; + } + + /** + * Verify organization permissions on a folder + */ + private function verify_organization_permissions($folder_id) { + try { + $response = $this->auth->make_drive_api_request('GET', "files/{$folder_id}/permissions"); + + if (isset($response['permissions'])) { + foreach ($response['permissions'] as $permission) { + if (isset($permission['domain']) && $permission['domain'] === 'measurequick.com' && + $permission['role'] === 'writer') { + return true; + } + } + } + + return false; + + } catch (Exception $e) { + $this->log_error("Failed to verify permissions on {$folder_id}: " . $e->getMessage()); + return false; + } + } + + /** + * Log info message + */ + private function log_info($message) { + if ($this->logger) { + $this->logger->info($message, 'Google Sheets Folders'); + } + } + + /** + * Log error message + */ + private function log_error($message) { + if ($this->logger) { + $this->logger->error($message, 'Google Sheets Folders'); + } + } +} +?> \ No newline at end of file diff --git a/includes/google-sheets/class-google-sheets-manager.php b/includes/google-sheets/class-google-sheets-manager.php new file mode 100644 index 00000000..bdbe3595 --- /dev/null +++ b/includes/google-sheets/class-google-sheets-manager.php @@ -0,0 +1,660 @@ +auth = new HVAC_Google_Sheets_Auth(); + + // Initialize folder manager only if the class exists + if (class_exists('HVAC_Google_Sheets_Folder_Manager')) { + $this->folder_manager = new HVAC_Google_Sheets_Folder_Manager(); + } + + // Load master dashboard data class + if (class_exists('HVAC_Master_Dashboard_Data')) { + $this->master_dashboard_data = new HVAC_Master_Dashboard_Data(); + } + + // Load logger if available + if (class_exists('HVAC_Logger')) { + $this->logger = new HVAC_Logger(); + } + } + + /** + * Create Master Report spreadsheet with 4 tabs + */ + public function create_master_report() { + try { + if (!$this->auth->is_authenticated()) { + throw new Exception('Google Sheets not authenticated'); + } + + // Get master trainer folder ID + $master_folder_id = null; + if ($this->folder_manager) { + $master_folder_id = $this->folder_manager->get_master_trainer_folder_id(); + } + if (!$master_folder_id) { + throw new Exception('Failed to get/create master trainer folder'); + } + + $spreadsheet_data = array( + 'properties' => array( + 'title' => 'HVAC Master Report - ' . date('Y-m-d H:i:s') + ), + 'parents' => array($master_folder_id), + 'sheets' => array( + array( + 'properties' => array( + 'title' => 'System Overview', + 'index' => 0 + ) + ), + array( + 'properties' => array( + 'title' => 'Trainer Performance', + 'index' => 1 + ) + ), + array( + 'properties' => array( + 'title' => 'All Events', + 'index' => 2 + ) + ), + array( + 'properties' => array( + 'title' => 'Revenue Analytics', + 'index' => 3 + ) + ) + ) + ); + + // First create the spreadsheet using Sheets API + $sheet_data = array( + 'properties' => $spreadsheet_data['properties'], + 'sheets' => $spreadsheet_data['sheets'] + ); + + $response = $this->auth->make_api_request('POST', '', $sheet_data); + + // Then move it to the correct folder using Drive API + if (isset($response['spreadsheetId'])) { + $this->move_file_to_folder($response['spreadsheetId'], $master_folder_id); + } + + if (isset($response['spreadsheetId'])) { + $spreadsheet_id = $response['spreadsheetId']; + + // Populate each tab with data + $this->populate_system_overview_tab($spreadsheet_id); + $this->populate_trainer_performance_tab($spreadsheet_id); + $this->populate_all_events_tab($spreadsheet_id); + $this->populate_revenue_analytics_tab($spreadsheet_id); + + // Store spreadsheet info + $this->store_master_report_info($spreadsheet_id, $response['spreadsheetUrl']); + + $this->log_info("Master Report created: {$spreadsheet_id}"); + + return array( + 'success' => true, + 'spreadsheet_id' => $spreadsheet_id, + 'url' => $response['spreadsheetUrl'] + ); + } + + throw new Exception('Failed to create spreadsheet'); + + } catch (Exception $e) { + $this->log_error('Failed to create Master Report: ' . $e->getMessage()); + return array( + 'success' => false, + 'error' => $e->getMessage() + ); + } + } + + /** + * Create Event-specific spreadsheet with 3 tabs + */ + public function create_event_spreadsheet($event_id) { + try { + if (!$this->auth->is_authenticated()) { + throw new Exception('Google Sheets not authenticated'); + } + + $event = get_post($event_id); + if (!$event) { + throw new Exception('Event not found'); + } + + // Get event-specific folder ID + $event_folder_id = null; + if ($this->folder_manager) { + $event_folder_id = $this->folder_manager->get_event_folder_id($event_id); + } + if (!$event_folder_id) { + // If no folder manager, use the root folder or throw exception + throw new Exception('Failed to get/create event folder - folder manager not available'); + } + + $spreadsheet_data = array( + 'properties' => array( + 'title' => 'Event Report - ' . $event->post_title . ' - ' . date('Y-m-d') + ), + 'parents' => array($event_folder_id), + 'sheets' => array( + array( + 'properties' => array( + 'title' => 'Event Details', + 'index' => 0 + ) + ), + array( + 'properties' => array( + 'title' => 'Attendees', + 'index' => 1 + ) + ), + array( + 'properties' => array( + 'title' => 'Financial Summary', + 'index' => 2 + ) + ) + ) + ); + + // First create the spreadsheet using Sheets API + $sheet_data = array( + 'properties' => $spreadsheet_data['properties'], + 'sheets' => $spreadsheet_data['sheets'] + ); + + $response = $this->auth->make_api_request('POST', '', $sheet_data); + + // Then move it to the correct folder using Drive API + if (isset($response['spreadsheetId'])) { + $this->move_file_to_folder($response['spreadsheetId'], $event_folder_id); + } + + if (isset($response['spreadsheetId'])) { + $spreadsheet_id = $response['spreadsheetId']; + + // Populate each tab with event data + $this->populate_event_details_tab($spreadsheet_id, $event_id); + $this->populate_event_attendees_tab($spreadsheet_id, $event_id); + $this->populate_event_financial_tab($spreadsheet_id, $event_id); + + // Store event spreadsheet info + $this->store_event_spreadsheet_info($event_id, $spreadsheet_id, $response['spreadsheetUrl']); + + $this->log_info("Event spreadsheet created for event {$event_id}: {$spreadsheet_id}"); + + return array( + 'success' => true, + 'spreadsheet_id' => $spreadsheet_id, + 'url' => $response['spreadsheetUrl'] + ); + } + + throw new Exception('Failed to create event spreadsheet'); + + } catch (Exception $e) { + $this->log_error("Failed to create event spreadsheet for {$event_id}: " . $e->getMessage()); + return array( + 'success' => false, + 'error' => $e->getMessage() + ); + } + } + + /** + * Populate System Overview tab + */ + private function populate_system_overview_tab($spreadsheet_id) { + if (!$this->master_dashboard_data) { + return; + } + + $data = array( + 'range' => 'System Overview!A1:B10', + 'majorDimension' => 'ROWS', + 'valueInputOption' => 'USER_ENTERED', + 'values' => array( + array('HVAC Community Events - System Overview', ''), + array('Generated', date('Y-m-d H:i:s')), + array('', ''), + array('Metric', 'Value'), + array('Total Events', $this->master_dashboard_data->get_total_events_count()), + array('Upcoming Events', $this->master_dashboard_data->get_upcoming_events_count()), + array('Completed Events', $this->master_dashboard_data->get_completed_events_count()), + array('Active Trainers', $this->master_dashboard_data->get_active_trainers_count()), + array('Tickets Sold', $this->master_dashboard_data->get_total_tickets_sold()), + array('Total Revenue', '$' . number_format($this->master_dashboard_data->get_total_revenue(), 2)) + ) + ); + + $endpoint = "/{$spreadsheet_id}/values/System Overview!A1:B10"; + $this->auth->make_api_request('PUT', $endpoint, $data); + } + + /** + * Populate Trainer Performance tab + */ + private function populate_trainer_performance_tab($spreadsheet_id) { + if (!$this->master_dashboard_data) { + return; + } + + $trainer_data = $this->master_dashboard_data->get_trainer_performance_data(); + + $values = array( + array('Trainer Performance Analytics', '', '', '', ''), + array('Generated', date('Y-m-d H:i:s'), '', '', ''), + array('', '', '', '', ''), + array('Trainer', 'Events', 'Tickets Sold', 'Revenue', 'Avg Revenue/Event') + ); + + foreach ($trainer_data as $trainer) { + $avg_revenue = $trainer['events'] > 0 ? $trainer['revenue'] / $trainer['events'] : 0; + $values[] = array( + $trainer['name'], + $trainer['events'], + $trainer['tickets'], + '$' . number_format($trainer['revenue'], 2), + '$' . number_format($avg_revenue, 2) + ); + } + + $data = array( + 'range' => 'Trainer Performance!A1:E' . (count($values)), + 'majorDimension' => 'ROWS', + 'valueInputOption' => 'USER_ENTERED', + 'values' => $values + ); + + $endpoint = "/{$spreadsheet_id}/values/Trainer Performance!A1:E" . (count($values)); + $this->auth->make_api_request('PUT', $endpoint, $data); + } + + /** + * Populate All Events tab + */ + private function populate_all_events_tab($spreadsheet_id) { + if (!$this->master_dashboard_data) { + return; + } + + $events_data = $this->master_dashboard_data->get_all_events_data(); + + $values = array( + array('All Events Management', '', '', '', '', ''), + array('Generated', date('Y-m-d H:i:s'), '', '', '', ''), + array('', '', '', '', '', ''), + array('Event Title', 'Trainer', 'Date', 'Status', 'Tickets', 'Revenue') + ); + + foreach ($events_data as $event) { + $values[] = array( + $event['title'], + $event['trainer_name'], + $event['date'], + $event['status'], + $event['tickets'], + '$' . number_format($event['revenue'], 2) + ); + } + + $data = array( + 'range' => 'All Events!A1:F' . (count($values)), + 'majorDimension' => 'ROWS', + 'valueInputOption' => 'USER_ENTERED', + 'values' => $values + ); + + $endpoint = "/{$spreadsheet_id}/values/All Events!A1:F" . (count($values)); + $this->auth->make_api_request('PUT', $endpoint, $data); + } + + /** + * Populate Revenue Analytics tab + */ + private function populate_revenue_analytics_tab($spreadsheet_id) { + if (!$this->master_dashboard_data) { + return; + } + + $monthly_data = $this->master_dashboard_data->get_monthly_revenue_data(); + + $values = array( + array('Revenue Analytics', '', ''), + array('Generated', date('Y-m-d H:i:s'), ''), + array('', '', ''), + array('Month', 'Events', 'Revenue') + ); + + foreach ($monthly_data as $month_data) { + $values[] = array( + $month_data['month'], + $month_data['events'], + '$' . number_format($month_data['revenue'], 2) + ); + } + + $data = array( + 'range' => 'Revenue Analytics!A1:C' . (count($values)), + 'majorDimension' => 'ROWS', + 'valueInputOption' => 'USER_ENTERED', + 'values' => $values + ); + + $endpoint = "/{$spreadsheet_id}/values/Revenue Analytics!A1:C" . (count($values)); + $this->auth->make_api_request('PUT', $endpoint, $data); + } + + /** + * Populate Event Details tab + */ + private function populate_event_details_tab($spreadsheet_id, $event_id) { + $event = get_post($event_id); + $event_meta = get_post_meta($event_id); + + $values = array( + array('Event Details Report', ''), + array('Generated', date('Y-m-d H:i:s')), + array('', ''), + array('Field', 'Value'), + array('Event Title', $event->post_title), + array('Event Date', get_post_meta($event_id, '_EventStartDate', true)), + array('Event Time', get_post_meta($event_id, '_EventStartTime', true)), + array('Venue', get_post_meta($event_id, '_EventVenueName', true)), + array('Address', get_post_meta($event_id, '_EventAddress', true)), + array('Trainer', get_the_author_meta('display_name', $event->post_author)), + array('Status', $event->post_status), + array('Capacity', get_post_meta($event_id, '_EventCapacity', true)), + array('Description', wp_strip_all_tags($event->post_content)) + ); + + $data = array( + 'range' => 'Event Details!A1:B' . (count($values)), + 'majorDimension' => 'ROWS', + 'valueInputOption' => 'USER_ENTERED', + 'values' => $values + ); + + $endpoint = "/{$spreadsheet_id}/values/Event Details!A1:B" . (count($values)); + $this->auth->make_api_request('PUT', $endpoint, $data); + } + + /** + * Populate Event Attendees tab + */ + private function populate_event_attendees_tab($spreadsheet_id, $event_id) { + // Get attendees data for this event + global $wpdb; + + $attendees = $wpdb->get_results($wpdb->prepare( + "SELECT u.display_name, u.user_email, um.meta_value as phone + FROM {$wpdb->posts} p + JOIN {$wpdb->users} u ON p.post_author = u.ID + LEFT JOIN {$wpdb->usermeta} um ON u.ID = um.user_id AND um.meta_key = 'phone' + WHERE p.post_parent = %d AND p.post_type = 'tribe_rsvp_attendees'", + $event_id + )); + + $values = array( + array('Event Attendees', '', ''), + array('Generated', date('Y-m-d H:i:s'), ''), + array('', '', ''), + array('Name', 'Email', 'Phone') + ); + + foreach ($attendees as $attendee) { + $values[] = array( + $attendee->display_name, + $attendee->user_email, + $attendee->phone ?: 'N/A' + ); + } + + $data = array( + 'range' => 'Attendees!A1:C' . (count($values)), + 'majorDimension' => 'ROWS', + 'valueInputOption' => 'USER_ENTERED', + 'values' => $values + ); + + $endpoint = "/{$spreadsheet_id}/values/Attendees!A1:C" . (count($values)); + $this->auth->make_api_request('PUT', $endpoint, $data); + } + + /** + * Populate Event Financial tab + */ + private function populate_event_financial_tab($spreadsheet_id, $event_id) { + // Calculate financial data for this event + $ticket_sales = $this->calculate_event_revenue($event_id); + $capacity = get_post_meta($event_id, '_EventCapacity', true); + $sold_tickets = count($this->get_event_attendees($event_id)); + + $values = array( + array('Financial Summary', ''), + array('Generated', date('Y-m-d H:i:s')), + array('', ''), + array('Metric', 'Value'), + array('Ticket Price', '$' . number_format($ticket_sales['price_per_ticket'], 2)), + array('Tickets Sold', $sold_tickets), + array('Capacity', $capacity), + array('Total Revenue', '$' . number_format($ticket_sales['total_revenue'], 2)), + array('Capacity Utilization', round(($sold_tickets / max($capacity, 1)) * 100, 1) . '%'), + array('Average Revenue per Attendee', '$' . number_format($sold_tickets > 0 ? $ticket_sales['total_revenue'] / $sold_tickets : 0, 2)) + ); + + $data = array( + 'range' => 'Financial Summary!A1:B' . (count($values)), + 'majorDimension' => 'ROWS', + 'valueInputOption' => 'USER_ENTERED', + 'values' => $values + ); + + $endpoint = "/{$spreadsheet_id}/values/Financial Summary!A1:B" . (count($values)); + $this->auth->make_api_request('PUT', $endpoint, $data); + } + + /** + * Store Master Report info in WordPress options + */ + private function store_master_report_info($spreadsheet_id, $url) { + $report_info = array( + 'spreadsheet_id' => $spreadsheet_id, + 'url' => $url, + 'created_at' => current_time('mysql'), + 'created_by' => get_current_user_id() + ); + + update_option('hvac_master_report_latest', $report_info); + + // Also store in history + $history = get_option('hvac_master_report_history', array()); + $history[] = $report_info; + // Keep only last 10 reports + if (count($history) > 10) { + $history = array_slice($history, -10); + } + update_option('hvac_master_report_history', $history); + } + + /** + * Store Event spreadsheet info + */ + private function store_event_spreadsheet_info($event_id, $spreadsheet_id, $url) { + $spreadsheet_info = array( + 'spreadsheet_id' => $spreadsheet_id, + 'url' => $url, + 'created_at' => current_time('mysql'), + 'created_by' => get_current_user_id() + ); + + update_post_meta($event_id, '_hvac_google_sheet', $spreadsheet_info); + } + + /** + * Get latest Master Report info + */ + public function get_latest_master_report() { + return get_option('hvac_master_report_latest', null); + } + + /** + * Get Master Report history + */ + public function get_master_report_history() { + return get_option('hvac_master_report_history', array()); + } + + /** + * Get Event spreadsheet info + */ + public function get_event_spreadsheet($event_id) { + return get_post_meta($event_id, '_hvac_google_sheet', true); + } + + /** + * Helper: Calculate event revenue + */ + private function calculate_event_revenue($event_id) { + global $wpdb; + + // Get ticket data for this event + $ticket_data = $wpdb->get_row($wpdb->prepare( + "SELECT + COUNT(attendees.ID) as tickets_sold, + MAX(CAST(ticket_meta.meta_value AS DECIMAL(10,2))) as ticket_price + FROM {$wpdb->posts} tickets + LEFT JOIN {$wpdb->posts} attendees ON tickets.ID = attendees.post_parent + LEFT JOIN {$wpdb->postmeta} ticket_meta ON tickets.ID = ticket_meta.post_id + AND ticket_meta.meta_key = '_ticket_price' + WHERE tickets.post_parent = %d + AND tickets.post_type = 'tribe_rsvp_tickets'", + $event_id + )); + + $price_per_ticket = $ticket_data->ticket_price ?: 0; + $tickets_sold = $ticket_data->tickets_sold ?: 0; + + return array( + 'price_per_ticket' => $price_per_ticket, + 'tickets_sold' => $tickets_sold, + 'total_revenue' => $price_per_ticket * $tickets_sold + ); + } + + /** + * Helper: Get event attendees + */ + private function get_event_attendees($event_id) { + global $wpdb; + + return $wpdb->get_results($wpdb->prepare( + "SELECT attendees.ID, attendees.post_title as name + FROM {$wpdb->posts} tickets + JOIN {$wpdb->posts} attendees ON tickets.ID = attendees.post_parent + WHERE tickets.post_parent = %d + AND tickets.post_type = 'tribe_rsvp_tickets' + AND attendees.post_type = 'tribe_rsvp_attendees'", + $event_id + )); + } + + /** + * Log info message + */ + private function log_info($message) { + if ($this->logger) { + $this->logger->info($message, 'GoogleSheets'); + } + error_log("HVAC Google Sheets: {$message}"); + } + + /** + * Log error message + */ + private function log_error($message) { + if ($this->logger) { + $this->logger->error($message, 'GoogleSheets'); + } + error_log("HVAC Google Sheets Error: {$message}"); + } + + /** + * Get authentication status + */ + public function get_auth_status() { + return $this->auth->get_config_status(); + } + + /** + * Test connection + */ + public function test_connection() { + return $this->auth->test_connection(); + } + + /** + * Move a file to a specific folder using Drive API + */ + private function move_file_to_folder($file_id, $folder_id) { + try { + // Get current parents + $file_info = $this->auth->make_drive_api_request('GET', "files/{$file_id}", null, array('fields' => 'parents')); + + if (!isset($file_info['parents'])) { + return false; + } + + $previous_parents = implode(',', $file_info['parents']); + + // Move to new folder + $response = $this->auth->make_drive_api_request('PATCH', "files/{$file_id}", null, array( + 'addParents' => $folder_id, + 'removeParents' => $previous_parents + )); + + $this->log_info("Moved file {$file_id} to folder {$folder_id}"); + return true; + + } catch (Exception $e) { + $this->log_error("Failed to move file {$file_id} to folder {$folder_id}: " . $e->getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/includes/google-sheets/google-sheets-config-template.php b/includes/google-sheets/google-sheets-config-template.php new file mode 100644 index 00000000..326b53bc --- /dev/null +++ b/includes/google-sheets/google-sheets-config-template.php @@ -0,0 +1,47 @@ + Create Credentials > OAuth 2.0 Client IDs + * 5. Set up the consent screen if required + * 6. Add authorized redirect URIs + * 7. Copy the Client ID and Client Secret above + * 8. Save this file as google-sheets-config.php + * 9. Use the Google Sheets admin page to authorize access + */ \ No newline at end of file diff --git a/includes/google-sheets/google-sheets-config.php b/includes/google-sheets/google-sheets-config.php new file mode 100644 index 00000000..f7b8b5ce --- /dev/null +++ b/includes/google-sheets/google-sheets-config.php @@ -0,0 +1,46 @@ +attendee_id) ? $attendee->attendee_id : + (isset($attendee->ID) ? $attendee->ID : 0); + } else { + $attendee_id = intval($attendee); + } + + if (!$attendee_id) { + return ''; + } + + // Get profile page URL + $profile_page = get_page_by_path('attendee-profile'); + if (!$profile_page) { + return ''; + } + + $profile_url = add_query_arg('attendee_id', $attendee_id, get_permalink($profile_page->ID)); + + // Default link text + if (empty($link_text)) { + $link_text = __('View Profile', 'hvac-community-events'); + } + + // Build classes + $class_list = array('hvac-attendee-profile-link'); + if (!empty($classes)) { + $class_list = array_merge($class_list, (array)$classes); + } + + // Generate link + return sprintf( + '%s ', + esc_url($profile_url), + esc_attr(implode(' ', $class_list)), + esc_attr__('View attendee profile', 'hvac-community-events'), + esc_html($link_text) + ); +} + +/** + * Generate a small icon link to view an attendee's profile + * + * @param int|object $attendee Attendee ID or attendee object + * @return string HTML icon link to attendee profile + */ +function hvac_get_attendee_profile_icon($attendee) { + // Get attendee ID + $attendee_id = 0; + if (is_object($attendee)) { + $attendee_id = isset($attendee->attendee_id) ? $attendee->attendee_id : + (isset($attendee->ID) ? $attendee->ID : 0); + } else { + $attendee_id = intval($attendee); + } + + if (!$attendee_id) { + return ''; + } + + // Get profile page URL + $profile_page = get_page_by_path('attendee-profile'); + if (!$profile_page) { + return ''; + } + + $profile_url = add_query_arg('attendee_id', $attendee_id, get_permalink($profile_page->ID)); + + // Generate icon link + return sprintf( + '', + esc_url($profile_url), + esc_attr__('View attendee profile', 'hvac-community-events') + ); +} + +/** + * Add profile link styles to pages that show attendee lists + */ +function hvac_attendee_profile_link_styles() { + ?> + + "; + $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 .= ""; + + fwrite($conn, $response); + fclose($conn); + + echo "Authorization code received: $auth_code\n"; + echo "Copy this code to your terminal.\n"; + + // Keep server running to display the page + sleep(10); + break; + } else { + // Send 404 for other requests + $response = "HTTP/1.1 404 Not Found\r\n"; + $response .= "Content-Type: text/html\r\n\r\n"; + $response .= "

404 Not Found

"; + + fwrite($conn, $response); + fclose($conn); + } +} + +fclose($server); +echo "\nServer stopped.\n"; \ No newline at end of file diff --git a/includes/zoho/check-permissions.php b/includes/zoho/check-permissions.php new file mode 100644 index 00000000..1302a48d --- /dev/null +++ b/includes/zoho/check-permissions.php @@ -0,0 +1,144 @@ +user_login . "\n"; + echo "Can manage options: " . (current_user_can('manage_options') ? 'Yes' : 'No') . "\n"; + } else { + echo "Could not check user capabilities\n"; + } +} + +echo "\n=== Check Completed ===\n"; +echo "If you see any 'Failed' or 'No' responses, they may indicate issues with your Zoho CRM integration.\n"; +echo "See the diagnostic log for more details about the connection test failures."; \ No newline at end of file diff --git a/includes/zoho/class-zoho-admin.php b/includes/zoho/class-zoho-admin.php new file mode 100644 index 00000000..2f21675b --- /dev/null +++ b/includes/zoho/class-zoho-admin.php @@ -0,0 +1,211 @@ +exchange_code_for_tokens($_GET['code'])) { + add_settings_error( + 'hvac_zoho_messages', + 'hvac_zoho_auth_success', + 'Successfully connected to Zoho CRM!', + 'success' + ); + } else { + add_settings_error( + 'hvac_zoho_messages', + 'hvac_zoho_auth_error', + 'Failed to connect to Zoho CRM. Please check your credentials.', + 'error' + ); + } + + // Redirect to remove code from URL + wp_redirect(admin_url('edit.php?post_type=tribe_events&page=hvac-zoho-crm')); + exit; + } + } + + /** + * Display admin page + */ + public function admin_page() { + ?> +
+

Zoho CRM Integration

+ + + + +
+

Zoho CRM configuration file not found. Please follow the setup instructions below.

+
+ +

Setup Instructions

+
    +
  1. + Register your application in Zoho: + Go to Zoho API Console +
  2. +
  3. Create a new Server-based Application
  4. +
  5. Set redirect URI to:
  6. +
  7. Copy your Client ID and Client Secret
  8. +
  9. Run the setup helper script from command line: +
    cd zoho
    +php setup-helper.php
    +
  10. +
+ + + + make_api_request('/crm/v2/org'); + $connected = !is_wp_error($org_info) && isset($org_info['org']); + ?> + + +
+

✓ Connected to Zoho CRM

+
+ +

Organization Information

+ + + + + + + + + + + + + +
Organization Name
Organization ID
Time Zone
+ +

Integration Status

+ display_integration_status(); ?> + +

Actions

+

+ Test Sync + Create Custom Fields +

+ + +
+

✗ Not connected to Zoho CRM

+
+ +

Reconnect to Zoho

+

Click the button below to authorize this application with Zoho CRM:

+

+ Connect to Zoho CRM +

+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ModuleFields ConfiguredLast SyncStatus
Campaigns (Events)check_custom_fields('Campaigns'); ?>
Contacts (Users)check_custom_fields('Contacts'); ?>
Invoices (Orders)check_custom_fields('Invoices'); ?>
+ Pending'; + } +} + +// Initialize admin interface +if (is_admin()) { + new HVAC_Zoho_Admin(); +} \ No newline at end of file diff --git a/includes/zoho/class-zoho-crm-auth.php b/includes/zoho/class-zoho-crm-auth.php new file mode 100644 index 00000000..b94a6fa3 --- /dev/null +++ b/includes/zoho/class-zoho-crm-auth.php @@ -0,0 +1,427 @@ +client_id = get_option('hvac_zoho_client_id', ''); + $this->client_secret = get_option('hvac_zoho_client_secret', ''); + $this->refresh_token = get_option('hvac_zoho_refresh_token', ''); + $this->redirect_uri = get_site_url() . '/oauth/callback'; + + // Fallback to config file if options are empty (backward compatibility) + if (empty($this->client_id) || empty($this->client_secret)) { + $config_file = plugin_dir_path(__FILE__) . 'zoho-config.php'; + if (file_exists($config_file)) { + require_once $config_file; + + $this->client_id = empty($this->client_id) && defined('ZOHO_CLIENT_ID') ? ZOHO_CLIENT_ID : $this->client_id; + $this->client_secret = empty($this->client_secret) && defined('ZOHO_CLIENT_SECRET') ? ZOHO_CLIENT_SECRET : $this->client_secret; + $this->refresh_token = empty($this->refresh_token) && defined('ZOHO_REFRESH_TOKEN') ? ZOHO_REFRESH_TOKEN : $this->refresh_token; + $this->redirect_uri = defined('ZOHO_REDIRECT_URI') ? ZOHO_REDIRECT_URI : $this->redirect_uri; + } + } + + // Load stored access token from WordPress options + $this->load_access_token(); + } + + /** + * Generate authorization URL for initial setup + */ + public function get_authorization_url() { + $params = array( + 'scope' => 'ZohoCRM.settings.ALL,ZohoCRM.modules.ALL,ZohoCRM.users.ALL,ZohoCRM.org.ALL,ZohoCRM.bulk.READ', + 'client_id' => $this->client_id, + 'response_type' => 'code', + 'access_type' => 'offline', + 'redirect_uri' => $this->redirect_uri, + 'prompt' => 'consent' + ); + + return 'https://accounts.zoho.com/oauth/v2/auth?' . http_build_query($params); + } + + /** + * Exchange authorization code for tokens + */ + public function exchange_code_for_tokens($auth_code) { + $url = 'https://accounts.zoho.com/oauth/v2/token'; + + $params = array( + 'grant_type' => 'authorization_code', + 'client_id' => $this->client_id, + 'client_secret' => $this->client_secret, + 'redirect_uri' => $this->redirect_uri, + 'code' => $auth_code + ); + + $response = wp_remote_post($url, array( + 'body' => $params, + 'headers' => array( + 'Content-Type' => 'application/x-www-form-urlencoded' + ) + )); + + if (is_wp_error($response)) { + $this->log_error('Failed to exchange code: ' . $response->get_error_message()); + return false; + } + + $body = wp_remote_retrieve_body($response); + $data = json_decode($body, true); + + if (isset($data['access_token']) && isset($data['refresh_token'])) { + $this->access_token = $data['access_token']; + $this->refresh_token = $data['refresh_token']; + $this->token_expiry = time() + $data['expires_in']; + + // Save tokens + $this->save_tokens(); + + return true; + } + + $this->log_error('Invalid token response: ' . $body); + return false; + } + + /** + * Get valid access token (refresh if needed) + */ + public function get_access_token() { + // Check if token is expired or will expire soon (5 mins buffer) + if (!$this->access_token || (time() + 300) >= $this->token_expiry) { + $this->refresh_access_token(); + } + + return $this->access_token; + } + + /** + * Refresh access token using refresh token + */ + private function refresh_access_token() { + $url = 'https://accounts.zoho.com/oauth/v2/token'; + + $params = array( + 'refresh_token' => $this->refresh_token, + 'client_id' => $this->client_id, + 'client_secret' => $this->client_secret, + 'grant_type' => 'refresh_token' + ); + + $response = wp_remote_post($url, array( + 'body' => $params, + 'headers' => array( + 'Content-Type' => 'application/x-www-form-urlencoded' + ) + )); + + if (is_wp_error($response)) { + $this->log_error('Failed to refresh token: ' . $response->get_error_message()); + return false; + } + + $body = wp_remote_retrieve_body($response); + $data = json_decode($body, true); + + if (isset($data['access_token'])) { + $this->access_token = $data['access_token']; + $this->token_expiry = time() + $data['expires_in']; + + $this->save_access_token(); + + return true; + } + + $this->log_error('Failed to refresh token: ' . $body); + return false; + } + + /** + * Make authenticated API request + */ + public function make_api_request($endpoint, $method = 'GET', $data = null) { + // Check if we're in staging mode + $site_url = get_site_url(); + $is_staging = strpos($site_url, 'upskillhvac.com') === false; + + // In staging mode, only allow read operations, no writes + if ($is_staging && in_array($method, array('POST', 'PUT', 'DELETE', 'PATCH'))) { + $this->log_debug('STAGING MODE: Simulating ' . $method . ' request to ' . $endpoint); + return array( + 'data' => array( + array( + 'code' => 'STAGING_MODE', + 'details' => array( + 'message' => 'Staging mode active. Write operations are disabled.' + ), + 'message' => 'This would have been a ' . $method . ' request to: ' . $endpoint, + 'status' => 'success' + ) + ) + ); + } + + // Debug logging of config status + if (defined('ZOHO_DEBUG_MODE') && ZOHO_DEBUG_MODE) { + $config_status = $this->get_configuration_status(); + $this->log_debug('Configuration status: ' . json_encode($config_status)); + + if (!$config_status['client_id_exists']) { + $this->log_error('Client ID is missing or empty'); + } + + if (!$config_status['client_secret_exists']) { + $this->log_error('Client Secret is missing or empty'); + } + + if (!$config_status['refresh_token_exists']) { + $this->log_error('Refresh Token is missing or empty'); + } + + if ($config_status['token_expired']) { + $this->log_debug('Access token is expired, will attempt to refresh'); + } + } + + $access_token = $this->get_access_token(); + + if (!$access_token) { + $error_message = 'No valid access token available'; + $this->log_error($error_message); + return new WP_Error('no_token', $error_message); + } + + $url = 'https://www.zohoapis.com/crm/v2' . $endpoint; + + // Log the request details + $this->log_debug('Making ' . $method . ' request to: ' . $url); + + $args = array( + 'method' => $method, + 'headers' => array( + 'Authorization' => 'Zoho-oauthtoken ' . $access_token, + 'Content-Type' => 'application/json' + ), + 'timeout' => 30 // Increase timeout to 30 seconds for potentially slow responses + ); + + if ($data && in_array($method, array('POST', 'PUT', 'PATCH'))) { + $args['body'] = json_encode($data); + $this->log_debug('Request payload: ' . json_encode($data)); + } + + // Execute the request + $this->log_debug('Executing request to Zoho API'); + $response = wp_remote_request($url, $args); + + // Handle WordPress errors + if (is_wp_error($response)) { + $error_message = 'API request failed: ' . $response->get_error_message(); + $error_data = $response->get_error_data(); + + $this->log_error($error_message); + $this->log_debug('Error details: ' . json_encode($error_data)); + + return $response; + } + + // Get response code and body + $status_code = wp_remote_retrieve_response_code($response); + $headers = wp_remote_retrieve_headers($response); + $body = wp_remote_retrieve_body($response); + + $this->log_debug('Response code: ' . $status_code); + + // Log headers for debugging + if (defined('ZOHO_DEBUG_MODE') && ZOHO_DEBUG_MODE) { + $this->log_debug('Response headers: ' . json_encode($headers->getAll())); + } + + // Handle empty responses + if (empty($body)) { + $error_message = 'Empty response received from Zoho API'; + $this->log_error($error_message); + return array( + 'error' => $error_message, + 'code' => $status_code, + 'details' => 'No response body received' + ); + } + + // Parse the JSON response + $data = json_decode($body, true); + + // Check for JSON parsing errors + if ($data === null && json_last_error() !== JSON_ERROR_NONE) { + $error_message = 'Invalid JSON response: ' . json_last_error_msg(); + $this->log_error($error_message); + $this->log_debug('Raw response: ' . $body); + + return array( + 'error' => $error_message, + 'code' => 'JSON_PARSE_ERROR', + 'details' => 'Raw response: ' . substr($body, 0, 255) . (strlen($body) > 255 ? '...' : '') + ); + } + + // Log response for debugging + if (defined('ZOHO_DEBUG_MODE') && ZOHO_DEBUG_MODE) { + $this->log_debug('API Response: ' . $body); + } + + // Check for API errors + if ($status_code >= 400) { + $error_message = isset($data['message']) ? $data['message'] : 'API error with status code ' . $status_code; + $this->log_error($error_message); + + // Add HTTP error information to the response + $data['http_status'] = $status_code; + $data['error'] = $error_message; + + // Extract more detailed error information if available + if (isset($data['code'])) { + $this->log_debug('Error code: ' . $data['code']); + } + + if (isset($data['details'])) { + $this->log_debug('Error details: ' . json_encode($data['details'])); + } + } + + return $data; + } + + /** + * Save tokens to WordPress options + */ + private function save_tokens() { + update_option('hvac_zoho_refresh_token', $this->refresh_token); + $this->save_access_token(); + } + + /** + * Save access token + */ + private function save_access_token() { + update_option('hvac_zoho_access_token', $this->access_token); + update_option('hvac_zoho_token_expiry', $this->token_expiry); + } + + /** + * Load access token from WordPress options + */ + private function load_access_token() { + $this->access_token = get_option('hvac_zoho_access_token'); + $this->token_expiry = get_option('hvac_zoho_token_expiry', 0); + + // Load refresh token if not set + if (!$this->refresh_token) { + $this->refresh_token = get_option('hvac_zoho_refresh_token'); + } + } + + /** + * Log error messages + */ + private function log_error($message) { + $this->last_error = $message; + + if (defined('ZOHO_LOG_FILE')) { + error_log('[' . date('Y-m-d H:i:s') . '] ERROR: ' . $message . PHP_EOL, 3, ZOHO_LOG_FILE); + } + + // Also log to WordPress debug log if available + if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { + error_log('[ZOHO CRM] ' . $message); + } + } + + /** + * Log debug messages + */ + private function log_debug($message) { + if (defined('ZOHO_DEBUG_MODE') && ZOHO_DEBUG_MODE && defined('ZOHO_LOG_FILE')) { + error_log('[' . date('Y-m-d H:i:s') . '] DEBUG: ' . $message . PHP_EOL, 3, ZOHO_LOG_FILE); + } + + // Also log to WordPress debug log if available + if (defined('ZOHO_DEBUG_MODE') && ZOHO_DEBUG_MODE && defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { + error_log('[ZOHO CRM DEBUG] ' . $message); + } + } + + /** + * Get the last error message + * + * @return string|null + */ + public function get_last_error() { + return $this->last_error; + } + + /** + * Get client ID (for debugging only) + * + * @return string + */ + public function get_client_id() { + return $this->client_id; + } + + /** + * Check if client secret exists (for debugging only) + * + * @return bool + */ + public function get_client_secret() { + return !empty($this->client_secret); + } + + /** + * Check if refresh token exists (for debugging only) + * + * @return bool + */ + public function get_refresh_token() { + return !empty($this->refresh_token); + } + + /** + * Get configuration status (for debugging) + * + * @return array + */ + public function get_configuration_status() { + return array( + 'client_id_exists' => !empty($this->client_id), + 'client_secret_exists' => !empty($this->client_secret), + 'refresh_token_exists' => !empty($this->refresh_token), + 'access_token_exists' => !empty($this->access_token), + 'token_expired' => $this->token_expiry < time(), + 'config_loaded' => file_exists(plugin_dir_path(__FILE__) . 'zoho-config.php') + ); + } +} \ No newline at end of file diff --git a/includes/zoho/class-zoho-sync.php b/includes/zoho/class-zoho-sync.php new file mode 100644 index 00000000..8523d543 --- /dev/null +++ b/includes/zoho/class-zoho-sync.php @@ -0,0 +1,428 @@ +auth = new HVAC_Zoho_CRM_Auth(); + + // Determine if we're in staging mode + $site_url = get_site_url(); + $this->is_staging = strpos($site_url, 'upskillhvac.com') === false; + } + + /** + * Check if sync is allowed + * + * @return bool + */ + private function is_sync_allowed() { + // Only allow sync on production (upskillhvac.com) + $site_url = get_site_url(); + return strpos($site_url, 'upskillhvac.com') !== false; + } + + /** + * Sync events to Zoho Campaigns + * + * @return array Sync results + */ + public function sync_events() { + $results = array( + 'total' => 0, + 'synced' => 0, + 'failed' => 0, + 'errors' => array(), + 'staging_mode' => $this->is_staging + ); + + // Get all published events + $events = tribe_get_events(array( + 'posts_per_page' => -1, + 'eventDisplay' => 'list', + 'meta_query' => array( + array( + 'key' => '_hvac_event_type', + 'value' => 'trainer', + 'compare' => '=' + ) + ) + )); + + $results['total'] = count($events); + + // If staging mode, simulate the sync + if ($this->is_staging) { + $results['message'] = 'STAGING MODE: Sync simulation only. No data sent to Zoho.'; + $results['synced'] = $results['total']; + $results['test_data'] = array(); + + foreach ($events as $event) { + $campaign_data = $this->prepare_campaign_data($event); + $results['test_data'][] = array( + 'event_id' => $event->ID, + 'event_title' => get_the_title($event), + 'zoho_data' => $campaign_data + ); + } + + return $results; + } + + // Production sync + if (!$this->is_sync_allowed()) { + $results['errors'][] = 'Sync not allowed on this domain. Only upskillhvac.com can sync to production.'; + return $results; + } + + foreach ($events as $event) { + try { + $campaign_data = $this->prepare_campaign_data($event); + + // Check if campaign already exists in Zoho + $search_response = $this->auth->make_api_request('GET', '/crm/v2/Campaigns/search', array( + 'criteria' => "(Campaign_Name:equals:{$campaign_data['Campaign_Name']})" + )); + + if (!empty($search_response['data'])) { + // Update existing campaign + $campaign_id = $search_response['data'][0]['id']; + $update_response = $this->auth->make_api_request('PUT', "/crm/v2/Campaigns/{$campaign_id}", array( + 'data' => array($campaign_data) + )); + } else { + // Create new campaign + $create_response = $this->auth->make_api_request('POST', '/crm/v2/Campaigns', array( + 'data' => array($campaign_data) + )); + } + + $results['synced']++; + + // Update event meta with Zoho ID + if (isset($campaign_id)) { + update_post_meta($event->ID, '_zoho_campaign_id', $campaign_id); + } + + } catch (Exception $e) { + $results['failed']++; + $results['errors'][] = sprintf('Event %s: %s', $event->ID, $e->getMessage()); + } + } + + return $results; + } + + /** + * Sync users to Zoho Contacts + * + * @return array Sync results + */ + public function sync_users() { + $results = array( + 'total' => 0, + 'synced' => 0, + 'failed' => 0, + 'errors' => array(), + 'staging_mode' => $this->is_staging + ); + + // Get trainers and attendees + $users = get_users(array( + 'role__in' => array('trainer', 'trainee'), + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => '_sync_to_zoho', + 'value' => '1', + 'compare' => '=' + ), + array( + 'key' => '_sync_to_zoho', + 'compare' => 'NOT EXISTS' + ) + ) + )); + + $results['total'] = count($users); + + // If staging mode, simulate the sync + if ($this->is_staging) { + $results['message'] = 'STAGING MODE: Sync simulation only. No data sent to Zoho.'; + $results['synced'] = $results['total']; + $results['test_data'] = array(); + + foreach ($users as $user) { + $contact_data = $this->prepare_contact_data($user); + $results['test_data'][] = array( + 'user_id' => $user->ID, + 'user_email' => $user->user_email, + 'user_role' => implode(', ', $user->roles), + 'zoho_data' => $contact_data + ); + } + + return $results; + } + + // Production sync + if (!$this->is_sync_allowed()) { + $results['errors'][] = 'Sync not allowed on this domain. Only upskillhvac.com can sync to production.'; + return $results; + } + + foreach ($users as $user) { + try { + $contact_data = $this->prepare_contact_data($user); + + // Check if contact already exists in Zoho + $search_response = $this->auth->make_api_request('GET', '/crm/v2/Contacts/search', array( + 'criteria' => "(Email:equals:{$contact_data['Email']})" + )); + + if (!empty($search_response['data'])) { + // Update existing contact + $contact_id = $search_response['data'][0]['id']; + $update_response = $this->auth->make_api_request('PUT', "/crm/v2/Contacts/{$contact_id}", array( + 'data' => array($contact_data) + )); + } else { + // Create new contact + $create_response = $this->auth->make_api_request('POST', '/crm/v2/Contacts', array( + 'data' => array($contact_data) + )); + + if (!empty($create_response['data'][0]['details']['id'])) { + $contact_id = $create_response['data'][0]['details']['id']; + } + } + + $results['synced']++; + + // Update user meta with Zoho ID + if (isset($contact_id)) { + update_user_meta($user->ID, '_zoho_contact_id', $contact_id); + } + + } catch (Exception $e) { + $results['failed']++; + $results['errors'][] = sprintf('User %s: %s', $user->ID, $e->getMessage()); + } + } + + return $results; + } + + /** + * Sync ticket purchases to Zoho Invoices + * + * @return array Sync results + */ + public function sync_purchases() { + $results = array( + 'total' => 0, + 'synced' => 0, + 'failed' => 0, + 'errors' => array(), + 'staging_mode' => $this->is_staging + ); + + // Get all completed orders + $orders = wc_get_orders(array( + 'status' => 'completed', + 'limit' => -1, + 'meta_key' => '_tribe_tickets_event_id', + 'meta_compare' => 'EXISTS' + )); + + $results['total'] = count($orders); + + // If staging mode, simulate the sync + if ($this->is_staging) { + $results['message'] = 'STAGING MODE: Sync simulation only. No data sent to Zoho.'; + $results['synced'] = $results['total']; + $results['test_data'] = array(); + + foreach ($orders as $order) { + $invoice_data = $this->prepare_invoice_data($order); + $results['test_data'][] = array( + 'order_id' => $order->get_id(), + 'order_number' => $order->get_order_number(), + 'order_total' => $order->get_total(), + 'zoho_data' => $invoice_data + ); + } + + return $results; + } + + // Production sync + if (!$this->is_sync_allowed()) { + $results['errors'][] = 'Sync not allowed on this domain. Only upskillhvac.com can sync to production.'; + return $results; + } + + foreach ($orders as $order) { + try { + $invoice_data = $this->prepare_invoice_data($order); + + // Check if invoice already exists in Zoho + $order_number = $order->get_order_number(); + $search_response = $this->auth->make_api_request('GET', '/crm/v2/Invoices/search', array( + 'criteria' => "(Invoice_Number:equals:{$order_number})" + )); + + if (!empty($search_response['data'])) { + // Update existing invoice + $invoice_id = $search_response['data'][0]['id']; + $update_response = $this->auth->make_api_request('PUT', "/crm/v2/Invoices/{$invoice_id}", array( + 'data' => array($invoice_data) + )); + } else { + // Create new invoice + $create_response = $this->auth->make_api_request('POST', '/crm/v2/Invoices', array( + 'data' => array($invoice_data) + )); + } + + $results['synced']++; + + // Update order meta with Zoho ID + if (isset($invoice_id)) { + $order->update_meta_data('_zoho_invoice_id', $invoice_id); + $order->save(); + } + + } catch (Exception $e) { + $results['failed']++; + $results['errors'][] = sprintf('Order %s: %s', $order->get_id(), $e->getMessage()); + } + } + + return $results; + } + + /** + * Prepare campaign data for Zoho + * + * @param WP_Post $event Event post object + * @return array Campaign data + */ + private function prepare_campaign_data($event) { + $trainer_id = get_post_meta($event->ID, '_trainer_id', true); + $trainer = get_user_by('id', $trainer_id); + $venue = tribe_get_venue($event->ID); + + return array( + 'Campaign_Name' => get_the_title($event->ID), + 'Start_Date' => tribe_get_start_date($event->ID, false, 'Y-m-d'), + 'End_Date' => tribe_get_end_date($event->ID, false, 'Y-m-d'), + 'Status' => (tribe_get_end_date($event->ID, false, 'U') < time()) ? 'Completed' : 'Active', + 'Description' => get_the_content(null, false, $event), + 'Type' => 'Training Event', + 'Expected_Revenue' => floatval(get_post_meta($event->ID, '_price', true)), + 'Total_Capacity' => intval(get_post_meta($event->ID, '_stock', true)), + 'Venue' => $venue ? get_the_title($venue) : '', + 'Trainer_Name' => $trainer ? $trainer->display_name : '', + 'Trainer_Email' => $trainer ? $trainer->user_email : '', + 'WordPress_Event_ID' => $event->ID + ); + } + + /** + * Prepare contact data for Zoho + * + * @param WP_User $user User object + * @return array Contact data + */ + private function prepare_contact_data($user) { + $role = in_array('trainer', $user->roles) ? 'Trainer' : 'Trainee'; + + return array( + 'First_Name' => get_user_meta($user->ID, 'first_name', true), + 'Last_Name' => get_user_meta($user->ID, 'last_name', true), + 'Email' => $user->user_email, + 'Phone' => get_user_meta($user->ID, 'phone_number', true), + 'Title' => get_user_meta($user->ID, 'hvac_professional_title', true), + 'Company' => get_user_meta($user->ID, 'hvac_company_name', true), + 'Lead_Source' => 'HVAC Community Events', + 'Contact_Type' => $role, + 'WordPress_User_ID' => $user->ID, + 'License_Number' => get_user_meta($user->ID, 'hvac_license_number', true), + 'Years_Experience' => get_user_meta($user->ID, 'hvac_years_experience', true), + 'Certification' => get_user_meta($user->ID, 'hvac_certifications', true) + ); + } + + /** + * Prepare invoice data for Zoho + * + * @param WC_Order $order Order object + * @return array Invoice data + */ + private function prepare_invoice_data($order) { + $event_id = $order->get_meta('_tribe_tickets_event_id'); + $event_title = get_the_title($event_id); + $customer = $order->get_user(); + + // Get contact ID from Zoho + $contact_id = null; + if ($customer) { + $contact_id = get_user_meta($customer->ID, '_zoho_contact_id', true); + } + + $items = array(); + foreach ($order->get_items() as $item) { + $items[] = array( + 'Product_Name' => $item->get_name(), + 'Quantity' => $item->get_quantity(), + 'Rate' => $item->get_subtotal() / $item->get_quantity(), + 'Total' => $item->get_total() + ); + } + + return array( + 'Invoice_Number' => $order->get_order_number(), + 'Invoice_Date' => $order->get_date_created()->format('Y-m-d'), + 'Status' => 'Paid', + 'Contact_Name' => $contact_id, + 'Subject' => "Ticket Purchase - {$event_title}", + 'Sub_Total' => $order->get_subtotal(), + 'Tax' => $order->get_total_tax(), + 'Total' => $order->get_total(), + 'Balance' => 0, + 'WordPress_Order_ID' => $order->get_id(), + 'Product_Details' => $items + ); + } +} +?> \ No newline at end of file diff --git a/includes/zoho/diagnostics.php b/includes/zoho/diagnostics.php new file mode 100644 index 00000000..cb5e2c52 --- /dev/null +++ b/includes/zoho/diagnostics.php @@ -0,0 +1,224 @@ +\n"; + } +} + +// Start diagnostics +diagnostics_log('Starting Zoho CRM diagnostics'); + +// Check for required files +$required_files = array( + 'class-zoho-crm-auth.php' => dirname(__FILE__) . '/class-zoho-crm-auth.php', + 'zoho-config.php' => dirname(__FILE__) . '/zoho-config.php', +); + +$missing_files = array(); +foreach ($required_files as $name => $path) { + if (!file_exists($path)) { + $missing_files[] = $name; + diagnostics_log("Missing required file: $name", 'ERROR'); + } else { + diagnostics_log("Found required file: $name"); + } +} + +if (!empty($missing_files)) { + diagnostics_log('Diagnostics failed due to missing files', 'ERROR'); + die('Missing required files: ' . implode(', ', $missing_files)); +} + +// Check for config constants +require_once $required_files['zoho-config.php']; +$required_constants = array( + 'ZOHO_CLIENT_ID', + 'ZOHO_CLIENT_SECRET', + 'ZOHO_REFRESH_TOKEN', + 'ZOHO_ACCOUNTS_URL', + 'ZOHO_API_BASE_URL', +); + +$missing_constants = array(); +$empty_constants = array(); +foreach ($required_constants as $constant) { + if (!defined($constant)) { + $missing_constants[] = $constant; + diagnostics_log("Missing required constant: $constant", 'ERROR'); + } else { + $value = constant($constant); + if (empty($value)) { + $empty_constants[] = $constant; + diagnostics_log("Constant is empty: $constant", 'WARNING'); + } else { + // Mask the actual value for security + $masked_value = $constant === 'ZOHO_CLIENT_ID' ? substr($value, 0, 4) . '...' : '[MASKED]'; + diagnostics_log("Found constant: $constant = $masked_value"); + } + } +} + +if (!empty($missing_constants)) { + diagnostics_log('Diagnostics found missing constants', 'ERROR'); + echo 'Missing required constants: ' . implode(', ', $missing_constants) . "
\n"; +} + +if (!empty($empty_constants)) { + diagnostics_log('Diagnostics found empty constants', 'WARNING'); + echo 'Empty constants: ' . implode(', ', $empty_constants) . "
\n"; +} + +// Initialize Zoho CRM Auth +require_once $required_files['class-zoho-crm-auth.php']; +$auth = new HVAC_Zoho_CRM_Auth(); + +// Check the configuration status +$config_status = $auth->get_configuration_status(); +diagnostics_log('Configuration status: ' . json_encode($config_status)); + +foreach ($config_status as $key => $value) { + $status = $value ? 'OK' : 'FAIL'; + $type = $value ? 'INFO' : 'ERROR'; + diagnostics_log("$key: $status", $type); + + echo "$key: " . ($value ? '✅' : '❌') . "
\n"; +} + +// Test getting an access token +try { + diagnostics_log('Testing access token retrieval'); + $access_token = $auth->get_access_token(); + + if ($access_token) { + diagnostics_log('Successfully retrieved access token'); + echo "Access token retrieval: ✅
\n"; + } else { + diagnostics_log('Failed to retrieve access token', 'ERROR'); + echo "Access token retrieval: ❌
\n"; + } +} catch (Exception $e) { + diagnostics_log('Exception while retrieving access token: ' . $e->getMessage(), 'ERROR'); + echo "Access token retrieval exception: " . $e->getMessage() . "
\n"; +} + +// Test API connection +try { + diagnostics_log('Testing API connection'); + $response = $auth->make_api_request('/settings/modules', 'GET'); + + if (is_wp_error($response)) { + diagnostics_log('API connection failed: ' . $response->get_error_message(), 'ERROR'); + echo "API connection: ❌ - " . $response->get_error_message() . "
\n"; + } else if (isset($response['modules'])) { + $module_count = count($response['modules']); + diagnostics_log("API connection successful. Found $module_count modules."); + echo "API connection: ✅ - Found $module_count modules
\n"; + + // List first few modules + echo "Available Modules:
\n"; + echo "
    \n"; + $count = 0; + foreach ($response['modules'] as $module) { + if ($count++ < 5) { + echo "
  • " . $module['api_name'] . " (" . $module['plural_label'] . ")
  • \n"; + } + } + if ($module_count > 5) { + echo "
  • ... and " . ($module_count - 5) . " more
  • \n"; + } + echo "
\n"; + } else { + diagnostics_log('API connection failed: ' . json_encode($response), 'ERROR'); + echo "API connection: ❌ - Error response
\n"; + echo "
" . json_encode($response, JSON_PRETTY_PRINT) . "
\n"; + } +} catch (Exception $e) { + diagnostics_log('Exception while testing API connection: ' . $e->getMessage(), 'ERROR'); + echo "API connection exception: " . $e->getMessage() . "
\n"; +} + +// Environment information +echo "

Environment Information

\n"; +echo "
    \n"; +echo "
  • PHP Version: " . phpversion() . "
  • \n"; +echo "
  • WordPress Version: " . get_bloginfo('version') . "
  • \n"; +echo "
  • Site URL: " . get_site_url() . "
  • \n"; +echo "
  • Staging Mode: " . (strpos(get_site_url(), 'upskillhvac.com') === false ? 'Yes' : 'No') . "
  • \n"; +echo "
  • Zoho Debug Mode: " . (defined('ZOHO_DEBUG_MODE') && ZOHO_DEBUG_MODE ? 'Enabled' : 'Disabled') . "
  • \n"; +echo "
  • Diagnostic Log: " . ZOHO_LOG_FILE . "
  • \n"; +echo "
\n"; + +// Final diagnostics message +diagnostics_log('Zoho CRM diagnostics completed'); +echo "

Diagnostics completed. Check the log file for more details: " . ZOHO_LOG_FILE . "

\n"; + +// Include a simple CSS for better presentation +echo "\n"; \ No newline at end of file diff --git a/includes/zoho/setup-helper.php b/includes/zoho/setup-helper.php new file mode 100644 index 00000000..91c92e7c --- /dev/null +++ b/includes/zoho/setup-helper.php @@ -0,0 +1,155 @@ + $scopes, + 'client_id' => $client_id, + 'response_type' => 'code', + 'access_type' => 'offline', + 'redirect_uri' => $redirect_uri, + 'prompt' => 'consent' +]); + +echo "\nStep 2: Authorize the application\n"; +echo "--------------------------------\n"; +echo "Open this URL in your browser:\n\n"; +echo $auth_url . "\n\n"; +echo "After authorization, you'll be redirected to:\n"; +echo $redirect_uri . "?code=AUTH_CODE\n\n"; +echo "Enter the authorization code: "; +$auth_code = trim(fgets(STDIN)); + +// Step 3: Exchange code for tokens +echo "\nStep 3: Exchanging code for tokens...\n"; +echo "-----------------------------------\n"; + +$token_url = 'https://accounts.zoho.com/oauth/v2/token'; +$token_params = [ + 'grant_type' => 'authorization_code', + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'code' => $auth_code +]; + +$ch = curl_init($token_url); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($token_params)); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + +$response = curl_exec($ch); +$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($http_code !== 200) { + echo "Error: Failed to get tokens (HTTP $http_code)\n"; + echo "Response: " . $response . "\n"; + exit(1); +} + +$token_data = json_decode($response, true); + +if (!isset($token_data['access_token']) || !isset($token_data['refresh_token'])) { + echo "Error: Invalid token response\n"; + echo "Response: " . $response . "\n"; + exit(1); +} + +echo "Success! Tokens received.\n\n"; + +// Step 4: Get Organization ID +echo "Step 4: Getting organization ID...\n"; +echo "--------------------------------\n"; + +$org_url = 'https://www.zohoapis.com/crm/v2/org'; +$ch = curl_init($org_url); +curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Zoho-oauthtoken ' . $token_data['access_token'] +]); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + +$org_response = curl_exec($ch); +$org_data = json_decode($org_response, true); +curl_close($ch); + +$org_id = isset($org_data['org'][0]['id']) ? $org_data['org'][0]['id'] : 'NOT_FOUND'; + +// Step 5: Generate config file +echo "\nStep 5: Generating configuration\n"; +echo "-------------------------------\n"; + +$config_content = " $scopes, + 'client_id' => $client_id, + 'response_type' => 'code', + 'access_type' => 'offline', + 'redirect_uri' => $redirect_uri, + 'prompt' => 'consent' +]); + +echo "Step 1: Authorization\n"; +echo "--------------------\n"; +echo "Please open this URL in your browser:\n\n"; +echo $auth_url . "\n\n"; +echo "After authorization, you'll be redirected to:\n"; +echo $redirect_uri . "?code=AUTH_CODE\n\n"; +echo "Enter the authorization code from the URL: "; +$auth_code = trim(fgets(STDIN)); + +// Step 2: Exchange code for tokens +echo "\nStep 2: Exchanging code for tokens...\n"; +echo "-----------------------------------\n"; + +$token_url = 'https://accounts.zoho.com/oauth/v2/token'; +$token_params = [ + 'grant_type' => 'authorization_code', + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'code' => $auth_code +]; + +$ch = curl_init($token_url); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($token_params)); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + +$response = curl_exec($ch); +$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($http_code !== 200) { + echo "Error: Failed to get tokens (HTTP $http_code)\n"; + echo "Response: " . $response . "\n"; + exit(1); +} + +$token_data = json_decode($response, true); + +if (!isset($token_data['access_token']) || !isset($token_data['refresh_token'])) { + echo "Error: Invalid token response\n"; + echo "Response: " . $response . "\n"; + exit(1); +} + +echo "✓ Tokens received successfully\n"; +echo "Access Token: " . substr($token_data['access_token'], 0, 20) . "...\n"; +echo "Refresh Token: " . substr($token_data['refresh_token'], 0, 20) . "...\n\n"; + +// Step 3: Get Organization Info +echo "Step 3: Getting organization information...\n"; +echo "-----------------------------------------\n"; + +$org_url = 'https://www.zohoapis.com/crm/v2/org'; +$ch = curl_init($org_url); +curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Zoho-oauthtoken ' . $token_data['access_token'] +]); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + +$org_response = curl_exec($ch); +$org_data = json_decode($org_response, true); +curl_close($ch); + +if (isset($org_data['org'][0])) { + $org = $org_data['org'][0]; + echo "✓ Organization found\n"; + echo "Name: " . $org['company_name'] . "\n"; + echo "ID: " . $org['id'] . "\n"; + echo "Time Zone: " . $org['time_zone'] . "\n\n"; +} else { + echo "Error: Could not get organization info\n"; + echo "Response: " . $org_response . "\n"; +} + +// Step 4: Test Module Access +echo "Step 4: Testing module access...\n"; +echo "-------------------------------\n"; + +$modules = ['Campaigns', 'Contacts', 'Invoices']; +foreach ($modules as $module) { + $module_url = "https://www.zohoapis.com/crm/v2/settings/modules/$module"; + $ch = curl_init($module_url); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Zoho-oauthtoken ' . $token_data['access_token'] + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + $module_response = curl_exec($ch); + $module_data = json_decode($module_response, true); + curl_close($ch); + + if (isset($module_data['modules'][0])) { + echo "✓ $module module accessible\n"; + } else { + echo "✗ $module module not accessible\n"; + } +} + +// Step 5: Create configuration file +echo "\nStep 5: Creating configuration file...\n"; +echo "-------------------------------------\n"; + +$config_content = " $dir) { + $env_file = $dir . '/.env'; + $exists = file_exists($env_file); + $debug_info .= "Search path " . ($i+1) . ": $env_file - " . ($exists ? 'EXISTS' : 'NOT FOUND') . "\n"; + if ($exists) { + $debug_info .= "File size: " . filesize($env_file) . " bytes\n"; + } + } + + // Check if variables are set + $debug_info .= "getenv('ZOHO_CLIENT_ID'): " . (getenv('ZOHO_CLIENT_ID') ?: 'NOT SET') . "\n"; + $debug_info .= "\$_ENV['ZOHO_CLIENT_ID']: " . (isset($_ENV['ZOHO_CLIENT_ID']) ? $_ENV['ZOHO_CLIENT_ID'] : 'NOT SET') . "\n"; + + // Log to debug file + if (!defined('ZOHO_LOG_FILE')) { + $log_dir = dirname(dirname(__FILE__)) . '/logs'; + if (!file_exists($log_dir)) { + mkdir($log_dir, 0755, true); + } + define('ZOHO_LOG_FILE', $log_dir . '/zoho-debug.log'); + } + error_log($debug_info, 3, ZOHO_LOG_FILE); +} + +// Log directory setup +$log_dir = dirname(dirname(__FILE__)) . '/logs'; +if (!file_exists($log_dir)) { + mkdir($log_dir, 0755, true); +} + +// Load .env file directly if it exists and getenv() doesn't work +if (empty(getenv('ZOHO_CLIENT_ID')) && function_exists('load_env_file')) { + $env_file = defined('ABSPATH') ? ABSPATH . '.env' : __DIR__ . '/../../../../.env'; + if (file_exists($env_file)) { + load_env_file($env_file); + } +} + +// OAuth Client Credentials +// IMPORTANT: You need to fill these values with your Zoho OAuth credentials +if (!defined('ZOHO_CLIENT_ID')) { + $client_id = getenv('ZOHO_CLIENT_ID'); + if (empty($client_id) && isset($_ENV['ZOHO_CLIENT_ID'])) { + $client_id = $_ENV['ZOHO_CLIENT_ID']; + } + // If still empty, try manual .env parsing + if (empty($client_id)) { + $env_file = defined('ABSPATH') ? ABSPATH . '.env' : __DIR__ . '/../../../../.env'; + if (file_exists($env_file)) { + $content = file_get_contents($env_file); + if (preg_match('/ZOHO_CLIENT_ID=([^\r\n]+)/', $content, $matches)) { + $client_id = trim($matches[1]); + } + } + } + define('ZOHO_CLIENT_ID', $client_id ?: ''); +} +if (!defined('ZOHO_CLIENT_SECRET')) { + $client_secret = getenv('ZOHO_CLIENT_SECRET'); + if (empty($client_secret) && isset($_ENV['ZOHO_CLIENT_SECRET'])) { + $client_secret = $_ENV['ZOHO_CLIENT_SECRET']; + } + // If still empty, try manual .env parsing + if (empty($client_secret)) { + $env_file = defined('ABSPATH') ? ABSPATH . '.env' : __DIR__ . '/../../../../.env'; + if (file_exists($env_file)) { + $content = file_get_contents($env_file); + if (preg_match('/ZOHO_CLIENT_SECRET=([^\r\n]+)/', $content, $matches)) { + $client_secret = trim($matches[1]); + } + } + } + define('ZOHO_CLIENT_SECRET', $client_secret ?: ''); +} +// Get site URL from WordPress if available, otherwise use environment variable or detect from server +if (function_exists('get_site_url')) { + $site_url = get_site_url(); +} elseif (getenv('UPSKILL_STAGING_URL')) { + $site_url = getenv('UPSKILL_STAGING_URL'); +} elseif (isset($_SERVER['HTTP_HOST'])) { + // Auto-detect from server request + $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://'; + $site_url = $protocol . $_SERVER['HTTP_HOST']; +} else { + // Production fallback - use the production domain + $site_url = 'https://upskillhvac.com'; +} +$site_url = rtrim($site_url, '/'); + +if (!defined('ZOHO_REDIRECT_URI')) { + define('ZOHO_REDIRECT_URI', $site_url . '/oauth/callback'); +} + +// API Endpoints +if (!defined('ZOHO_ACCOUNTS_URL')) { + define('ZOHO_ACCOUNTS_URL', 'https://accounts.zoho.com'); +} +if (!defined('ZOHO_API_BASE_URL')) { + define('ZOHO_API_BASE_URL', 'https://www.zohoapis.com/crm/v2'); +} + +// Scopes +if (!defined('ZOHO_SCOPES')) { + define('ZOHO_SCOPES', 'ZohoCRM.settings.ALL,ZohoCRM.modules.ALL,ZohoCRM.users.ALL,ZohoCRM.org.ALL,ZohoCRM.bulk.READ'); +} + +// Optional - Refresh Token (if already obtained) +if (!defined('ZOHO_REFRESH_TOKEN')) { + $refresh_token = getenv('ZOHO_REFRESH_TOKEN'); + if (empty($refresh_token) && isset($_ENV['ZOHO_REFRESH_TOKEN'])) { + $refresh_token = $_ENV['ZOHO_REFRESH_TOKEN']; + } + // If still empty, try manual .env parsing + if (empty($refresh_token)) { + $env_file = defined('ABSPATH') ? ABSPATH . '.env' : __DIR__ . '/../../../../.env'; + if (file_exists($env_file)) { + $content = file_get_contents($env_file); + if (preg_match('/ZOHO_REFRESH_TOKEN=([^\r\n]+)/', $content, $matches)) { + $refresh_token = trim($matches[1]); + } + } + } + define('ZOHO_REFRESH_TOKEN', $refresh_token ?: ''); +} + +// Debug Settings - Enhanced for better logging +if (!defined('ZOHO_DEBUG_MODE')) { + define('ZOHO_DEBUG_MODE', true); +} +if (!defined('ZOHO_LOG_FILE')) { + define('ZOHO_LOG_FILE', $log_dir . '/zoho-debug.log'); +} + +// Add diagnostic information to log +if (defined('ZOHO_DEBUG_MODE') && ZOHO_DEBUG_MODE) { + $timestamp = date('Y-m-d H:i:s'); + $debug_info = "[{$timestamp}] Zoho CRM Configuration loaded\n"; + $debug_info .= "[{$timestamp}] .env file loaded: " . ($env_loaded ? 'Yes' : 'No') . "\n"; + $debug_info .= "[{$timestamp}] Client ID exists: " . (!empty(ZOHO_CLIENT_ID) ? 'Yes' : 'No') . "\n"; + $debug_info .= "[{$timestamp}] Client ID value: " . (ZOHO_CLIENT_ID ? substr(ZOHO_CLIENT_ID, 0, 5) . '...' : 'EMPTY') . "\n"; + $debug_info .= "[{$timestamp}] Client Secret exists: " . (!empty(ZOHO_CLIENT_SECRET) ? 'Yes' : 'No') . "\n"; + $debug_info .= "[{$timestamp}] Client Secret value: " . (ZOHO_CLIENT_SECRET ? substr(ZOHO_CLIENT_SECRET, 0, 5) . '...' : 'EMPTY') . "\n"; + $debug_info .= "[{$timestamp}] Refresh Token exists: " . (!empty(ZOHO_REFRESH_TOKEN) ? 'Yes' : 'No') . "\n"; + $debug_info .= "[{$timestamp}] Refresh Token value: " . (ZOHO_REFRESH_TOKEN ? substr(ZOHO_REFRESH_TOKEN, 0, 5) . '...' : 'EMPTY') . "\n"; + $debug_info .= "[{$timestamp}] Log file path: " . ZOHO_LOG_FILE . "\n"; + + if (function_exists('get_site_url')) { + $debug_info .= "[{$timestamp}] WordPress site URL: " . get_site_url() . "\n"; + $debug_info .= "[{$timestamp}] Staging mode: " . (strpos(get_site_url(), 'upskillhvac.com') === false ? 'Yes' : 'No') . "\n"; + $debug_info .= "[{$timestamp}] Using OAuth Redirect URI: " . ZOHO_REDIRECT_URI . "\n"; + } else { + $debug_info .= "[{$timestamp}] WordPress functions not available\n"; + $debug_info .= "[{$timestamp}] Using environment variable for domain: " . ($site_url ?? 'Not set') . "\n"; + $debug_info .= "[{$timestamp}] Using OAuth Redirect URI: " . ZOHO_REDIRECT_URI . "\n"; + } + + // Check for environment variables directly + $debug_info .= "[{$timestamp}] Environment variables:\n"; + $debug_info .= "[{$timestamp}] - _ENV['ZOHO_CLIENT_ID']: " . (isset($_ENV['ZOHO_CLIENT_ID']) ? 'Set' : 'Not set') . "\n"; + $debug_info .= "[{$timestamp}] - _ENV['ZOHO_CLIENT_SECRET']: " . (isset($_ENV['ZOHO_CLIENT_SECRET']) ? 'Set' : 'Not set') . "\n"; + $debug_info .= "[{$timestamp}] - _ENV['ZOHO_REFRESH_TOKEN']: " . (isset($_ENV['ZOHO_REFRESH_TOKEN']) ? 'Set' : 'Not set') . "\n"; + + // Log configuration details + error_log($debug_info, 3, ZOHO_LOG_FILE); +} \ No newline at end of file diff --git a/templates/attendee/template-attendee-profile.php b/templates/attendee/template-attendee-profile.php new file mode 100644 index 00000000..91fe42ac --- /dev/null +++ b/templates/attendee/template-attendee-profile.php @@ -0,0 +1,149 @@ + + +
+ + +
+
+

Attendee Profile

+ +
+ +
+ + +
+
+
+

Total Purchases

+
+
+
+
+
+

Events Registered

+
+
+
+
+
+

Events Attended

+
+
+
+
+
+

Certificates Earned

+
+
+
+
+ + +
+

Contact Information

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name
Email
Phone
Company
State
+
+
+ + +
+

Activity Timeline

+ + +

No activity recorded for this attendee.

+ +
+ $event): ?> +
+
+ + +
+
+ +
+
+

+ + + + + + + + Certificate # + + + + + View Event + + +
+ +
+ +
+ +
+ +
+
\ No newline at end of file diff --git a/templates/certificates/certificate-fix.php b/templates/certificates/certificate-fix.php new file mode 100644 index 00000000..d1487658 --- /dev/null +++ b/templates/certificates/certificate-fix.php @@ -0,0 +1,135 @@ + + +
+
+

Certificate System Diagnostics

+ +
+

Rewrite Rules

+

If certificate download URLs are returning 404 errors, flush the rewrite rules.

+ +
+ + +
+ +

Rewrite rules have been flushed!

'; + } + ?> + +

+ Test Certificate Rewrite Rules +

+
+ +
+

Certificate Database

+ prefix . 'hvac_certificates'; + + // Check if table exists + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$cert_table'") === $cert_table; + + if ($table_exists) { + $total = $wpdb->get_var("SELECT COUNT(*) FROM $cert_table"); + $active = $wpdb->get_var("SELECT COUNT(*) FROM $cert_table WHERE revoked = 0"); + $revoked = $wpdb->get_var("SELECT COUNT(*) FROM $cert_table WHERE revoked = 1"); + + echo "

✅ Certificate table exists

"; + echo "
    "; + echo "
  • Total certificates: $total
  • "; + echo "
  • Active certificates: $active
  • "; + echo "
  • Revoked certificates: $revoked
  • "; + echo "
"; + } else { + echo "

❌ Certificate table does not exist!

"; + } + ?> +
+ +
+

Certificate Files

+ ✅ Certificate directory exists: $cert_dir

"; + + // Count PDF files + $pdf_count = count(glob($cert_dir . '/*.pdf')); + echo "

Total PDF files: $pdf_count

"; + + // Check .htaccess + if (file_exists($cert_dir . '/.htaccess')) { + echo "

✅ .htaccess file exists for security

"; + } else { + echo "

⚠️ .htaccess file missing - certificates may not be protected

"; + } + } else { + echo "

❌ Certificate directory does not exist!

"; + } + ?> +
+ +
+

Recent Certificate Activity

+ get_results(" + SELECT c.*, p.post_title as event_title + FROM $cert_table c + LEFT JOIN {$wpdb->posts} p ON c.event_id = p.ID + ORDER BY c.generated_date DESC + LIMIT 10 + "); + + if ($recent) { + echo ''; + echo ''; + echo ''; + + foreach ($recent as $cert) { + $status = $cert->revoked ? 'Revoked' : 'Active'; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
IDEventGeneratedStatus
' . $cert->certificate_id . '' . esc_html($cert->event_title) . '' . date('Y-m-d H:i', strtotime($cert->generated_date)) . '' . $status . '
'; + } else { + echo '

No certificates found.

'; + } + } + ?> +
+
+ + + \ No newline at end of file diff --git a/templates/certificates/certificate-reports-content.php b/templates/certificates/certificate-reports-content.php new file mode 100644 index 00000000..354f0b05 --- /dev/null +++ b/templates/certificates/certificate-reports-content.php @@ -0,0 +1,230 @@ +'; + +// Get current user ID +$current_user_id = get_current_user_id(); + +// Initialize variables with defaults +$certificates = array(); +$certificate_stats = array('total' => 0, 'active' => 0, 'revoked' => 0, 'emailed' => 0); +$events = array(); +$filter_event = isset($_GET['filter_event']) ? absint($_GET['filter_event']) : 0; +$filter_status = isset($_GET['filter_status']) ? sanitize_text_field($_GET['filter_status']) : 'active'; + +// Removed problematic output buffering that interferes with WordPress header rendering + // Get user's events directly from database to bypass TEC issues + global $wpdb; + + $events_query = $wpdb->prepare(" + SELECT DISTINCT p.ID, p.post_title + FROM {$wpdb->posts} p + LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + WHERE p.post_type = 'tribe_events' + AND p.post_status = 'publish' + AND p.post_author = %d + ORDER BY p.post_date DESC + ", $current_user_id); + + $events_results = $wpdb->get_results($events_query); + + if ($events_results) { + foreach ($events_results as $event) { + $events[$event->ID] = $event->post_title; + } + } + + // Get certificate stats + $stats_query = $wpdb->prepare(" + SELECT + COUNT(DISTINCT c.id) as total, + COUNT(DISTINCT CASE WHEN c.status = 'active' THEN c.id END) as active, + COUNT(DISTINCT CASE WHEN c.status = 'revoked' THEN c.id END) as revoked, + COUNT(DISTINCT e.certificate_id) as emailed + FROM {$wpdb->prefix}hvac_certificates c + LEFT JOIN {$wpdb->prefix}hvac_certificate_emails e ON c.id = e.certificate_id + WHERE c.trainer_id = %d + ", $current_user_id); + + $stats = $wpdb->get_row($stats_query); + if ($stats) { + $certificate_stats = array( + 'total' => (int)$stats->total, + 'active' => (int)$stats->active, + 'revoked' => (int)$stats->revoked, + 'emailed' => (int)$stats->emailed + ); + } + + // Build certificate query + $cert_query = " + SELECT DISTINCT c.*, a.name as attendee_name, a.email as attendee_email, + e.post_title as event_name, pm.meta_value as event_date + FROM {$wpdb->prefix}hvac_certificates c + LEFT JOIN {$wpdb->prefix}hvac_attendees a ON c.attendee_id = a.id + LEFT JOIN {$wpdb->posts} e ON c.event_id = e.ID + LEFT JOIN {$wpdb->postmeta} pm ON e.ID = pm.post_id AND pm.meta_key = '_EventStartDate' + WHERE c.trainer_id = %d + "; + + $query_params = array($current_user_id); + + // Apply filters + if ($filter_event > 0) { + $cert_query .= " AND c.event_id = %d"; + $query_params[] = $filter_event; + } + + if ($filter_status && $filter_status !== 'all') { + $cert_query .= " AND c.status = %s"; + $query_params[] = $filter_status; + } + + $cert_query .= " ORDER BY c.date_generated DESC LIMIT 100"; + + $certificates = $wpdb->get_results($wpdb->prepare($cert_query, $query_params)); +?> + +
+
+

+

+
+ + +
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+

+
+
+
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+ + +
+

+ + +
+

+ +
+ +
+

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
certificate_number); ?> + attendee_name); ?>
+ attendee_email); ?> +
event_name); ?>date_generated))); ?> + + status)); ?> + + +
+ + + + status === 'active'): ?> + + + + + +
+
+
+ +
+
\ No newline at end of file diff --git a/templates/certificates/generate-certificates-content.php b/templates/certificates/generate-certificates-content.php new file mode 100644 index 00000000..48e68132 --- /dev/null +++ b/templates/certificates/generate-certificates-content.php @@ -0,0 +1,166 @@ +'; + +// Get current user ID +$current_user_id = get_current_user_id(); +$trainer_profile = get_user_meta($current_user_id, 'hvac_trainer_profile', true); + +// Get Events +global $wpdb; +$events_query = $wpdb->prepare(" + SELECT DISTINCT p.ID, p.post_title + FROM {$wpdb->posts} p + WHERE p.post_type = 'tribe_events' + AND p.post_status = 'publish' + AND p.post_author = %d + ORDER BY p.post_date DESC +", $current_user_id); + +$events = $wpdb->get_results($events_query); + +// Check for selected event +$selected_event_id = isset($_GET['event_id']) ? absint($_GET['event_id']) : 0; +$attendees = array(); + +if ($selected_event_id) { + // Get attendees for the selected event + $attendees_query = $wpdb->prepare(" + SELECT a.*, c.certificate_number + FROM {$wpdb->prefix}hvac_attendees a + LEFT JOIN {$wpdb->prefix}hvac_certificates c ON a.id = c.attendee_id AND c.event_id = %d + WHERE a.event_id = %d + ORDER BY a.name ASC + ", $selected_event_id, $selected_event_id); + + $attendees = $wpdb->get_results($attendees_query); +} +?> + +
+
+

+

+
+ + +
+

+ + +
+

+ +
+ +
+
+ + +
+
+ +
+ + + +
+

+ +
+ + + +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ certificate_number ? 'disabled' : ''; ?>> + name); ?>email); ?> + certificate_number): ?> + + + (certificate_number); ?>) + + + + + + +
+
+
+
+ +
+

+ + + +
+ +
+ + \ No newline at end of file diff --git a/templates/certificates/template-certificate-reports-fixed.php b/templates/certificates/template-certificate-reports-fixed.php new file mode 100644 index 00000000..c4d04e0d --- /dev/null +++ b/templates/certificates/template-certificate-reports-fixed.php @@ -0,0 +1,427 @@ + 0, 'active' => 0, 'revoked' => 0, 'emailed' => 0); +$events = array(); +$filter_event = isset($_GET['filter_event']) ? absint($_GET['filter_event']) : 0; +$filter_status = isset($_GET['filter_status']) ? sanitize_text_field($_GET['filter_status']) : 'active'; + +// Start output buffering to prevent issues +ob_start(); + +try { + // Get user's events directly from database to bypass TEC issues + global $wpdb; + + // Build author filter - only current user's events + $events = $wpdb->get_results($wpdb->prepare( + "SELECT ID, post_title, post_date + FROM {$wpdb->posts} + WHERE post_type = 'tribe_events' + AND post_author = %d + AND post_status = 'publish' + ORDER BY post_date DESC", + $current_user_id + )); + + // Check if certificate table exists + $cert_table = $wpdb->prefix . 'hvac_certificates'; + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$cert_table'") === $cert_table; + + if ($table_exists && !empty($events)) { + // Get event IDs for the user + $event_ids = array_column($events, 'ID'); + $event_ids_placeholder = implode(',', array_fill(0, count($event_ids), '%d')); + + // Get certificate statistics + $total_certs = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM $cert_table WHERE event_id IN ($event_ids_placeholder)", + ...$event_ids + )); + + $active_certs = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM $cert_table WHERE event_id IN ($event_ids_placeholder) AND revoked = 0", + ...$event_ids + )); + + $revoked_certs = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM $cert_table WHERE event_id IN ($event_ids_placeholder) AND revoked = 1", + ...$event_ids + )); + + $emailed_certs = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM $cert_table WHERE event_id IN ($event_ids_placeholder) AND email_sent = 1", + ...$event_ids + )); + + $certificate_stats = array( + 'total' => intval($total_certs), + 'active' => intval($active_certs), + 'revoked' => intval($revoked_certs), + 'emailed' => intval($emailed_certs) + ); + + // Get certificates based on filters + $where_conditions = array("event_id IN ($event_ids_placeholder)"); + $query_params = $event_ids; + + // Add event filter if specified + if ($filter_event > 0 && in_array($filter_event, $event_ids)) { + $where_conditions = array("event_id = %d"); + $query_params = array($filter_event); + } + + // Add status filter + if ($filter_status === 'active') { + $where_conditions[] = "revoked = 0"; + } elseif ($filter_status === 'revoked') { + $where_conditions[] = "revoked = 1"; + } + + $where_clause = "WHERE " . implode(" AND ", $where_conditions); + + $certificates = $wpdb->get_results($wpdb->prepare( + "SELECT * FROM $cert_table $where_clause ORDER BY date_generated DESC LIMIT 50", + ...$query_params + )); + } + +} catch (Exception $e) { + // Log error but continue with empty data + error_log('Certificate Reports Error: ' . $e->getMessage()); +} + +// Clean output buffer +ob_end_clean(); + +// Get header +get_header(); +?> + +
+
+ +
+

Certificate Reports

+ +
+ +
+

View and manage all certificates you've generated for event attendees.

+
+ + +
+

Certificate Statistics

+ +
+
+

Total Certificates

+
+
+ +
+

Active Certificates

+
+
+ +
+

Revoked Certificates

+
+
+ +
+

Emailed Certificates

+
+
+
+
+ + +
+

Certificate Filters

+ +
+
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + +
+

Certificate Listing

+ + +
+

You don't have any events yet. Create your first event to start generating certificates.

+

Create Event

+
+ +
+

No certificates found matching your filters.

+ + 0 || $filter_status !== 'all') : ?> +

Clear filters to see all your certificates.

+ +

Generate certificates for your event attendees on the Generate Certificates page.

+ +
+ +
+ + + + + + + + + + + + + certificate_number ?? 'N/A'); + $event_id = intval($certificate->event_id ?? 0); + $attendee_id = intval($certificate->attendee_id ?? 0); + $generated_date = $certificate->date_generated ? date_i18n(get_option('date_format'), strtotime($certificate->date_generated)) : 'Unknown'; + $is_revoked = !empty($certificate->revoked); + $is_emailed = !empty($certificate->email_sent); + + // Get event and attendee information safely + $event_title = get_the_title($event_id) ?: 'Unknown Event'; + $attendee_name = get_post_meta($attendee_id, '_tribe_tickets_full_name', true) ?: 'Attendee #' . $attendee_id; + + // Status text and class + $status_text = $is_revoked ? 'Revoked' : 'Active'; + $status_class = $is_revoked ? 'hvac-status-revoked' : 'hvac-status-active'; + ?> + + + + + + + + + + +
Certificate #EventAttendeeDate GeneratedStatusActions
+ + + + + + + + + + + + + Certificate revoked + +
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/templates/certificates/template-certificate-reports-simple.php b/templates/certificates/template-certificate-reports-simple.php new file mode 100644 index 00000000..3bfc7567 --- /dev/null +++ b/templates/certificates/template-certificate-reports-simple.php @@ -0,0 +1,268 @@ +prefix . 'hvac_certificates'; + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name; + + if (!$table_exists) { + throw new Exception("Certificate database tables are not properly set up."); + } + + // Basic empty stats + $certificate_stats = array( + 'total' => 0, + 'active' => 0, + 'revoked' => 0, + 'emailed' => 0 + ); + + // Get user's events for filtering + $events = get_posts(array( + 'post_type' => 'tribe_events', + 'posts_per_page' => -1, + 'post_status' => 'publish', + 'author' => $current_user_id + )); + + // Get basic stats without using the certificate manager class + $event_ids = array(); + foreach ($events as $event) { + $event_ids[] = $event->ID; + } + + if (!empty($event_ids)) { + $event_ids_string = implode(',', array_map('intval', $event_ids)); + + // Only run query if we have events + if (!empty($event_ids_string)) { + $stats_query = "SELECT + COUNT(*) as total, + SUM(CASE WHEN revoked = 0 THEN 1 ELSE 0 END) as active, + SUM(CASE WHEN revoked = 1 THEN 1 ELSE 0 END) as revoked, + SUM(CASE WHEN email_sent = 1 THEN 1 ELSE 0 END) as emailed + FROM {$wpdb->prefix}hvac_certificates + WHERE event_id IN ($event_ids_string)"; + + $result = $wpdb->get_row($stats_query); + + if ($result) { + $certificate_stats = array( + 'total' => intval($result->total), + 'active' => intval($result->active), + 'revoked' => intval($result->revoked), + 'emailed' => intval($result->emailed) + ); + } + } + } + + // Empty certificates array to start with + $certificates = array(); + $total_certificates = 0; + $total_pages = 0; + $page = isset($_GET['certificate_page']) ? absint($_GET['certificate_page']) : 1; + $per_page = 20; + +} catch (Exception $e) { + echo '
Error: ' . esc_html($e->getMessage()) . '
'; +} +?> + +
+
+
+

Certificate Reports

+

View and manage all certificates you've generated for event attendees.

+
+ + +
+

Certificate Statistics

+ +
+
+

Total Certificates

+
+
+ +
+

Active Certificates

+
+
+ +
+

Revoked Certificates

+
+
+ +
+

Emailed Certificates

+
+
+
+
+ + +
+

Certificate Filters

+ +
+
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + +
+

Certificate Listing

+ + +
+

No certificates found matching your filters.

+ + 0 || (isset($_GET['filter_status']) && $_GET['filter_status'] !== 'active')) : ?> +

Clear filters to see all your certificates.

+ +

Generate certificates for your event attendees on the Generate Certificates page.

+ +
+ +
+ + + + + + + + + + + + + certificate_number; + $event_id = $certificate->event_id; + $attendee_id = $certificate->attendee_id; + $generated_date = date_i18n(get_option('date_format'), strtotime($certificate->date_generated)); + $is_revoked = (bool) $certificate->revoked; + $is_emailed = (bool) $certificate->email_sent; + + // Get event and attendee information + $event_title = get_the_title($event_id); + $attendee_name = get_post_meta($attendee_id, '_tribe_tickets_full_name', true); + if (empty($attendee_name)) { + $attendee_name = 'Attendee #' . $attendee_id; + } + + // Status text and class + $status_text = $is_revoked ? 'Revoked' : 'Active'; + $status_class = $is_revoked ? 'hvac-status-revoked' : 'hvac-status-active'; + ?> + + + + + + + + + + +
Certificate #EventAttendeeDate GeneratedStatusActions
+ + + + + + + + + + + + + + Certificate has been revoked + +
+
+ +
+ + +
+
+ × +

Certificate Preview

+ +
+
+
+ + admin_url('admin-ajax.php'), + 'viewNonce' => wp_create_nonce('hvac_view_certificate'), + 'emailNonce' => wp_create_nonce('hvac_email_certificate'), + 'revokeNonce' => wp_create_nonce('hvac_revoke_certificate') +)); + +// Footer +get_footer(); +?> \ No newline at end of file diff --git a/templates/certificates/template-certificate-reports.php b/templates/certificates/template-certificate-reports.php new file mode 100644 index 00000000..dbccabeb --- /dev/null +++ b/templates/certificates/template-certificate-reports.php @@ -0,0 +1,407 @@ + 0, 'active' => 0, 'revoked' => 0, 'emailed' => 0); +$events = array(); +$filter_event = isset($_GET['filter_event']) ? absint($_GET['filter_event']) : 0; +$filter_status = isset($_GET['filter_status']) ? sanitize_text_field($_GET['filter_status']) : 'active'; + +// Start output buffering to prevent issues +ob_start(); + +try { + // Get user's events directly from database to bypass TEC issues + global $wpdb; + + // Build author filter - only current user's events + $events = $wpdb->get_results($wpdb->prepare( + "SELECT ID, post_title, post_date + FROM {$wpdb->posts} + WHERE post_type = 'tribe_events' + AND post_author = %d + AND post_status = 'publish' + ORDER BY post_date DESC", + $current_user_id + )); + + // Check if certificate table exists + $cert_table = $wpdb->prefix . 'hvac_certificates'; + $table_exists = $wpdb->get_var("SHOW TABLES LIKE '$cert_table'") === $cert_table; + + if ($table_exists && !empty($events)) { + // Get event IDs for the user + $event_ids = array_column($events, 'ID'); + $event_ids_placeholder = implode(',', array_fill(0, count($event_ids), '%d')); + + // Get certificate statistics + $total_certs = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM $cert_table WHERE event_id IN ($event_ids_placeholder)", + ...$event_ids + )); + + $active_certs = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM $cert_table WHERE event_id IN ($event_ids_placeholder) AND revoked = 0", + ...$event_ids + )); + + $revoked_certs = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM $cert_table WHERE event_id IN ($event_ids_placeholder) AND revoked = 1", + ...$event_ids + )); + + $emailed_certs = $wpdb->get_var($wpdb->prepare( + "SELECT COUNT(*) FROM $cert_table WHERE event_id IN ($event_ids_placeholder) AND email_sent = 1", + ...$event_ids + )); + + $certificate_stats = array( + 'total' => intval($total_certs), + 'active' => intval($active_certs), + 'revoked' => intval($revoked_certs), + 'emailed' => intval($emailed_certs) + ); + + // Get certificates based on filters + $where_conditions = array("event_id IN ($event_ids_placeholder)"); + $query_params = $event_ids; + + // Add event filter if specified + if ($filter_event > 0 && in_array($filter_event, $event_ids)) { + $where_conditions = array("event_id = %d"); + $query_params = array($filter_event); + } + + // Add status filter + if ($filter_status === 'active') { + $where_conditions[] = "revoked = 0"; + } elseif ($filter_status === 'revoked') { + $where_conditions[] = "revoked = 1"; + } + + $where_clause = "WHERE " . implode(" AND ", $where_conditions); + + $certificates = $wpdb->get_results($wpdb->prepare( + "SELECT * FROM $cert_table $where_clause ORDER BY date_generated DESC LIMIT 50", + ...$query_params + )); + } + +} catch (Exception $e) { + // Log error but continue with empty data + error_log('Certificate Reports Error: ' . $e->getMessage()); +} + +// Clean output buffer +ob_end_clean(); +?> + +
+
+ +
+

Certificate Reports

+
+ +
+

View and manage all certificates you've generated for event attendees.

+
+ + +
+

Certificate Statistics

+ +
+
+

Total Certificates

+
+
+ +
+

Active Certificates

+
+
+ +
+

Revoked Certificates

+
+
+ +
+

Emailed Certificates

+
+
+
+
+ + +
+

Certificate Filters

+ +
+
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + +
+

Certificate Listing

+ + +
+

You don't have any events yet. Create your first event to start generating certificates.

+

Create Event

+
+ +
+

No certificates found matching your filters.

+ + 0 || $filter_status !== 'all') : ?> +

Clear filters to see all your certificates.

+ +

Generate certificates for your event attendees on the Generate Certificates page.

+ +
+ +
+ + + + + + + + + + + + + certificate_number ?? 'N/A'); + $event_id = intval($certificate->event_id ?? 0); + $attendee_id = intval($certificate->attendee_id ?? 0); + $generated_date = $certificate->date_generated ? date_i18n(get_option('date_format'), strtotime($certificate->date_generated)) : 'Unknown'; + $is_revoked = !empty($certificate->revoked); + $is_emailed = !empty($certificate->email_sent); + + // Get event and attendee information safely + $event_title = get_the_title($event_id) ?: 'Unknown Event'; + $attendee_name = get_post_meta($attendee_id, '_tribe_tickets_full_name', true) ?: 'Attendee #' . $attendee_id; + + // Status text and class + $status_text = $is_revoked ? 'Revoked' : 'Active'; + $status_class = $is_revoked ? 'hvac-status-revoked' : 'hvac-status-active'; + ?> + + + + + + + + + + +
Certificate #EventAttendeeDate GeneratedStatusActions
+ + + + + + + + + + + + + + + + + + + + + Certificate revoked + +
+
+ +
+
+
+ + diff --git a/templates/certificates/template-certificate-reports.php.backup b/templates/certificates/template-certificate-reports.php.backup new file mode 100644 index 00000000..9ab3f06b --- /dev/null +++ b/templates/certificates/template-certificate-reports.php.backup @@ -0,0 +1,328 @@ +check_tables(); + + if (!$tables_exist) { + echo '
Certificate database tables are not properly set up. Please contact the administrator.
'; + return; + } + + // Get filtering parameters + $filter_event = isset($_GET['filter_event']) ? absint($_GET['filter_event']) : 0; + $filter_status = isset($_GET['filter_status']) ? sanitize_text_field($_GET['filter_status']) : 'active'; + $page = isset($_GET['certificate_page']) ? absint($_GET['certificate_page']) : 1; + $per_page = 20; + + // Build filter args + $filter_args = array( + 'page' => $page, + 'per_page' => $per_page, + 'orderby' => 'date_generated', + 'order' => 'DESC', + ); + + // Add event filter if selected + if ($filter_event > 0) { + $filter_args['event_id'] = $filter_event; + } + + // Add status filter + if ($filter_status === 'active') { + $filter_args['revoked'] = 0; + } elseif ($filter_status === 'revoked') { + $filter_args['revoked'] = 1; + } + // Default 'all' doesn't add a filter + + // Get user's events for filtering using direct database query (bypassing TEC interference) + global $wpdb; + + // Build author filter + $author_filter = current_user_can('edit_others_posts') ? '' : 'AND post_author = ' . intval($current_user_id); + + // Get events directly from database + $events = $wpdb->get_results( + "SELECT ID, post_title, post_date + FROM {$wpdb->posts} + WHERE post_type = 'tribe_events' + AND post_status = 'publish' + {$author_filter} + ORDER BY post_date DESC" + ); + + // Check if user has any events + if (empty($events)) { + // No certificates to show since user has no events + $certificates = array(); + $total_certificates = 0; + $total_pages = 0; + $certificate_stats = array( + 'total' => 0, + 'active' => 0, + 'revoked' => 0, + 'emailed' => 0 + ); + } else { + // Get certificates for the current user with filters + $certificates = $certificate_manager->get_user_certificates($current_user_id, $filter_args); + + // Get total certificate count for pagination + $total_certificates = $certificate_manager->get_user_certificate_count($current_user_id, $filter_args); + $total_pages = ceil($total_certificates / $per_page); + + // Get certificate statistics + $certificate_stats = $certificate_manager->get_user_certificate_stats($current_user_id); + } + + // Get header and footer + get_header(); +} catch (Exception $e) { + echo '
Error initializing certificate system: ' . esc_html($e->getMessage()) . '
'; + return; +} +?> + +
+
+ +
+

Certificate Reports

+ +
+ +
+

View and manage all certificates you've generated for event attendees.

+
+ + +
+

Certificate Statistics

+ +
+
+

Total Certificates

+
+
+ +
+

Active Certificates

+
+
+ +
+

Revoked Certificates

+
+
+ +
+

Emailed Certificates

+
+
+
+
+ + +
+

Certificate Filters

+ +
+
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + +
+

Certificate Listing

+ + +
+

No certificates found matching your filters.

+ + 0 || $filter_status !== 'active') : ?> +

Clear filters to see all your certificates.

+ +

Generate certificates for your event attendees on the Generate Certificates page.

+ +
+ +
+ + + + + + + + + + + + + certificate_number; + $event_id = $certificate->event_id; + $attendee_id = $certificate->attendee_id; + $generated_date = date_i18n(get_option('date_format'), strtotime($certificate->date_generated)); + $is_revoked = (bool) $certificate->revoked; + $is_emailed = (bool) $certificate->email_sent; + + // Get event and attendee information + $event_title = get_the_title($event_id); + $attendee_name = get_post_meta($attendee_id, '_tribe_tickets_full_name', true); + if (empty($attendee_name)) { + $attendee_name = 'Attendee #' . $attendee_id; + } + + // Status text and class + $status_text = $is_revoked ? 'Revoked' : 'Active'; + $status_class = $is_revoked ? 'hvac-status-revoked' : 'hvac-status-active'; + ?> + + + + + + + + + + +
Certificate #EventAttendeeDate GeneratedStatusActions
+ + + + + + + + revoked_date)) : ?> +
+ revoked_date))); ?> +
+ +
+ + + + + + Certificate has been revoked + +
+
+ + 1) : ?> +
+ 1) { + $prev_url = add_query_arg('certificate_page', $page - 1); + echo '« Previous'; + } + + // Page numbers + for ($i = 1; $i <= $total_pages; $i++) { + $page_url = add_query_arg('certificate_page', $i); + $class = $i === $page ? 'hvac-button hvac-pagination-current' : 'hvac-button'; + echo '' . $i . ''; + } + + // Next page link + if ($page < $total_pages) { + $next_url = add_query_arg('certificate_page', $page + 1); + echo 'Next »'; + } + ?> +
+ + +
+ + +
+
+ × +

Certificate Preview

+ +
+
+
+ + admin_url('admin-ajax.php'), + 'viewNonce' => wp_create_nonce('hvac_view_certificate'), + 'emailNonce' => wp_create_nonce('hvac_email_certificate'), + 'revokeNonce' => wp_create_nonce('hvac_revoke_certificate') +)); + +// Close the try block +get_footer(); +?> diff --git a/templates/certificates/template-certificate-reports.php.bak b/templates/certificates/template-certificate-reports.php.bak new file mode 100644 index 00000000..15688d48 --- /dev/null +++ b/templates/certificates/template-certificate-reports.php.bak @@ -0,0 +1,346 @@ +check_tables(); + hvac_debug_log('Tables exist check result', $tables_exist); + + if (!$tables_exist) { + hvac_debug_log('Tables do not exist, showing error'); + echo '
Certificate database tables are not properly set up. Please contact the administrator.
'; + return; + } + + // Get filtering parameters + $filter_event = isset($_GET['filter_event']) ? absint($_GET['filter_event']) : 0; + $filter_status = isset($_GET['filter_status']) ? sanitize_text_field($_GET['filter_status']) : 'active'; + $page = isset($_GET['certificate_page']) ? absint($_GET['certificate_page']) : 1; + $per_page = 20; + + // Build filter args + $filter_args = array( + 'page' => $page, + 'per_page' => $per_page, + 'orderby' => 'date_generated', + 'order' => 'DESC', + ); + + // Add event filter if selected + if ($filter_event > 0) { + $filter_args['event_id'] = $filter_event; + } + + // Add status filter + if ($filter_status === 'active') { + $filter_args['revoked'] = 0; + } elseif ($filter_status === 'revoked') { + $filter_args['revoked'] = 1; + } + // Default 'all' doesn't add a filter + + // Get user's events for filtering + $args = array( + 'post_type' => Tribe__Events__Main::POSTTYPE, + 'posts_per_page' => -1, + 'post_status' => 'publish', + 'author' => $current_user_id, + 'orderby' => 'meta_value', + 'meta_key' => '_EventStartDate', + 'order' => 'DESC', + ); + + // Allow admins to see all events + if (current_user_can('edit_others_posts')) { + unset($args['author']); + } + + $events = get_posts($args); + + // Check if user has any events + if (empty($events)) { + // No certificates to show since user has no events + $certificates = array(); + $total_certificates = 0; + $total_pages = 0; + $certificate_stats = array( + 'total' => 0, + 'active' => 0, + 'revoked' => 0, + 'emailed' => 0 + ); + } else { + // Get certificates for the current user with filters + $certificates = $certificate_manager->get_user_certificates($current_user_id, $filter_args); + + // Get total certificate count for pagination + $total_certificates = $certificate_manager->get_user_certificate_count($current_user_id, $filter_args); + $total_pages = ceil($total_certificates / $per_page); + + // Get certificate statistics + $certificate_stats = $certificate_manager->get_user_certificate_stats($current_user_id); + } + + // Get header and footer + get_header(); +} catch (Exception $e) { + echo '
Error initializing certificate system: ' . esc_html($e->getMessage()) . '
'; + return; +} +?> + +
+
+
+

Certificate Reports

+

View and manage all certificates you've generated for event attendees.

+
+ + +
+

Certificate Statistics

+ +
+
+

Total Certificates

+
+
+ +
+

Active Certificates

+
+
+ +
+

Revoked Certificates

+
+
+ +
+

Emailed Certificates

+
+
+
+
+ + +
+

Certificate Filters

+ +
+
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + +
+

Certificate Listing

+ + +
+

No certificates found matching your filters.

+ + 0 || $filter_status !== 'active') : ?> +

Clear filters to see all your certificates.

+ +

Generate certificates for your event attendees on the Generate Certificates page.

+ +
+ +
+ + + + + + + + + + + + + certificate_number; + $event_id = $certificate->event_id; + $attendee_id = $certificate->attendee_id; + $generated_date = date_i18n(get_option('date_format'), strtotime($certificate->date_generated)); + $is_revoked = (bool) $certificate->revoked; + $is_emailed = (bool) $certificate->email_sent; + + // Get event and attendee information + $event_title = get_the_title($event_id); + $attendee_name = get_post_meta($attendee_id, '_tribe_tickets_full_name', true); + if (empty($attendee_name)) { + $attendee_name = 'Attendee #' . $attendee_id; + } + + // Status text and class + $status_text = $is_revoked ? 'Revoked' : 'Active'; + $status_class = $is_revoked ? 'hvac-status-revoked' : 'hvac-status-active'; + ?> + + + + + + + + + + +
Certificate #EventAttendeeDate GeneratedStatusActions
+ + + + + + + + revoked_date)) : ?> +
+ revoked_date))); ?> +
+ +
+ + + + + + Certificate has been revoked + +
+
+ + 1) : ?> +
+ 1) { + $prev_url = add_query_arg('certificate_page', $page - 1); + echo '« Previous'; + } + + // Page numbers + for ($i = 1; $i <= $total_pages; $i++) { + $page_url = add_query_arg('certificate_page', $i); + $class = $i === $page ? 'hvac-button hvac-pagination-current' : 'hvac-button'; + echo '' . $i . ''; + } + + // Next page link + if ($page < $total_pages) { + $next_url = add_query_arg('certificate_page', $page + 1); + echo 'Next »'; + } + ?> +
+ + +
+ + +
+
+ × +

Certificate Preview

+ +
+
+
+ + admin_url('admin-ajax.php'), + 'viewNonce' => wp_create_nonce('hvac_view_certificate'), + 'emailNonce' => wp_create_nonce('hvac_email_certificate'), + 'revokeNonce' => wp_create_nonce('hvac_revoke_certificate') +)); + +// Close the try block +get_footer(); +?> + +Error in certificate reports: ' . esc_html($e->getMessage()) . ''; +} +?> diff --git a/templates/certificates/template-generate-certificates-fixed.php b/templates/certificates/template-generate-certificates-fixed.php new file mode 100644 index 00000000..d80f141e --- /dev/null +++ b/templates/certificates/template-generate-certificates-fixed.php @@ -0,0 +1,461 @@ +get_results($wpdb->prepare( + "SELECT ID, post_title, post_date + FROM {$wpdb->posts} + WHERE post_type = 'tribe_events' + AND post_author = %d + AND post_status = 'publish' + ORDER BY post_date DESC", + $current_user_id + )); + + // If event is selected, get attendees + if ($event_id > 0) { + // Verify the event belongs to the current user + $event_found = false; + foreach ($events as $event) { + if ($event->ID == $event_id) { + $event_found = true; + $selected_event_title = $event->post_title; + break; + } + } + + if ($event_found) { + // Get attendees for the selected event + $attendees = $wpdb->get_results($wpdb->prepare( + "SELECT p.ID as attendee_id, + pm1.meta_value as holder_name, + pm2.meta_value as holder_email, + pm3.meta_value as check_in + FROM {$wpdb->posts} p + LEFT JOIN {$wpdb->postmeta} pm1 ON p.ID = pm1.post_id AND pm1.meta_key = '_tribe_tickets_full_name' + LEFT JOIN {$wpdb->postmeta} pm2 ON p.ID = pm2.post_id AND pm2.meta_key = '_tribe_tickets_email' + LEFT JOIN {$wpdb->postmeta} pm3 ON p.ID = pm3.post_id AND pm3.meta_key = '_tribe_tickets_checked_in' + LEFT JOIN {$wpdb->postmeta} pm4 ON p.ID = pm4.post_id AND pm4.meta_key = '_tribe_tickets_event' + WHERE p.post_type = 'tribe_ticket_attendee' + AND p.post_status = 'publish' + AND pm4.meta_value = %d + ORDER BY pm1.meta_value ASC", + $event_id + )); + } + } + +} catch (Exception $e) { + error_log('Generate Certificates Error: ' . $e->getMessage()); +} + +// Get header +get_header(); +?> + +
+
+ +
+

Generate Certificates

+ +
+ +
+

Generate certificates for attendees of your events.

+
+ + +
+

Step 1: Select Event

+ + +
+

You don't have any events yet. Create your first event to start generating certificates.

+

Create Event

+
+ +
+ + +
+ +
+ + + 0) : ?> +
+

Step 2: Select Attendees for ""

+ + +
+

This event has no attendees yet.

+

Attendees are created when people register for your event through the ticket system.

+
+ +
+ + + + +
+
+ + + +
+ +
+ + + + + + + + + + + check_in); + $checked_in_class = $checked_in ? 'hvac-checked-in' : ''; + $status_class = $checked_in ? 'hvac-status-checked-in' : 'hvac-status-not-checked-in'; + $status_text = $checked_in ? 'Checked In' : 'Not Checked In'; + $attendee_name = $attendee->holder_name ?: 'Unknown'; + $attendee_email = $attendee->holder_email ?: 'No email'; + ?> + + + + + + + + +
+ + Attendee NameEmailCheck-in Status
+ > + + + + +
+
+
+ +
+ +
+
+ +
+ + + + +
+
+ + + + + + \ No newline at end of file diff --git a/templates/certificates/template-generate-certificates.php b/templates/certificates/template-generate-certificates.php new file mode 100644 index 00000000..a3b4ddf8 --- /dev/null +++ b/templates/certificates/template-generate-certificates.php @@ -0,0 +1,534 @@ +get_results($wpdb->prepare( + "SELECT ID, post_title, post_date + FROM {$wpdb->posts} + WHERE post_type = 'tribe_events' + AND post_author = %d + AND post_status = 'publish' + ORDER BY post_date DESC", + $current_user_id + )); + + // If event is selected, get attendees + if ($event_id > 0) { + // Verify the event belongs to the current user + $event_found = false; + foreach ($events as $event) { + if ($event->ID == $event_id) { + $event_found = true; + $selected_event_title = $event->post_title; + break; + } + } + + if ($event_found) { + // Get attendees using the same query as the AJAX handler + $attendees = $wpdb->get_results($wpdb->prepare( + "SELECT + p.ID as attendee_id, + p.post_parent as event_id, + COALESCE(tec_full_name.meta_value, tpp_full_name.meta_value, tickets_full_name.meta_value, 'Unknown Attendee') as holder_name, + COALESCE(tec_email.meta_value, tpp_email.meta_value, tickets_email.meta_value, tpp_attendee_email.meta_value, 'no-email@example.com') as holder_email, + COALESCE(checked_in.meta_value, '0') as check_in + FROM {$wpdb->posts} p + LEFT JOIN {$wpdb->postmeta} tec_full_name ON p.ID = tec_full_name.post_id AND tec_full_name.meta_key = '_tec_tickets_commerce_full_name' + LEFT JOIN {$wpdb->postmeta} tpp_full_name ON p.ID = tpp_full_name.post_id AND tpp_full_name.meta_key = '_tribe_tpp_full_name' + LEFT JOIN {$wpdb->postmeta} tickets_full_name ON p.ID = tickets_full_name.post_id AND tickets_full_name.meta_key = '_tribe_tickets_full_name' + LEFT JOIN {$wpdb->postmeta} tec_email ON p.ID = tec_email.post_id AND tec_email.meta_key = '_tec_tickets_commerce_email' + LEFT JOIN {$wpdb->postmeta} tpp_email ON p.ID = tpp_email.post_id AND tpp_email.meta_key = '_tribe_tpp_email' + LEFT JOIN {$wpdb->postmeta} tickets_email ON p.ID = tickets_email.post_id AND tickets_email.meta_key = '_tribe_tickets_email' + LEFT JOIN {$wpdb->postmeta} tpp_attendee_email ON p.ID = tpp_attendee_email.post_id AND tpp_attendee_email.meta_key = '_tribe_tpp_attendee_email' + LEFT JOIN {$wpdb->postmeta} checked_in ON p.ID = checked_in.post_id AND checked_in.meta_key = '_tribe_tickets_attendee_checked_in' + WHERE p.post_type IN ('tec_tc_attendee', 'tribe_tpp_attendees') + AND p.post_parent = %d + ORDER BY p.ID ASC", + $event_id + )); + + // Check certificate status for each attendee + if (!empty($attendees) && class_exists('HVAC_Certificate_Manager')) { + $certificate_manager = HVAC_Certificate_Manager::instance(); + foreach ($attendees as $attendee) { + // Get the actual certificate data, not just boolean + $certificate = $certificate_manager->get_certificate_by_attendee($event_id, $attendee->attendee_id); + $attendee->has_certificate = !empty($certificate); + $attendee->certificate_data = $certificate; + } + } + + // Log for debugging if needed + if (defined('WP_DEBUG') && WP_DEBUG) { + error_log('Generate Certificates - Event ID: ' . $event_id . ', Attendees: ' . count($attendees)); + } + } + } + +} catch (Exception $e) { + error_log('Generate Certificates Error: ' . $e->getMessage()); +} +?> + +
+ render_trainer_menu(); + } + ?> + + render_breadcrumbs(); + } + ?> + +
+
+ +
+

Generate Certificates

+
+ +
+

Generate certificates for attendees of your events.

+
+ + +
+

Step 1: Select Event

+ + +
+

You don't have any events yet. Create your first event to start generating certificates.

+

Create Event

+
+ +
+ + +
+ +
+ + + 0) : ?> +
+

Step 2: Select Attendees for ""

+ + +
+

This event has no attendees yet.

+

Attendees are created when people register for your event through the ticket system.

+
+ +
+ + + + + has_certificate)) { + $has_certificate_count++; + } + } + + if ($has_certificate_count > 0) : ?> +
+

Note: attendee(s) already have certificates. These will be skipped to prevent duplicates.

+

Attendees with existing certificates are marked with ✓ and cannot be selected.

+
+ + +
+
+ + + +
+ +
+ + + + + + + + + + + + check_in); + $checked_in_class = $checked_in ? 'hvac-checked-in' : ''; + $status_class = $checked_in ? 'hvac-status-checked-in' : 'hvac-status-not-checked-in'; + $status_text = $checked_in ? 'Checked In' : 'Not Checked In'; + $attendee_name = $attendee->holder_name ?: 'Unknown'; + $attendee_email = $attendee->holder_email ?: 'No email'; + $has_certificate = !empty($attendee->has_certificate); + $cert_status_class = $has_certificate ? 'hvac-has-certificate' : ''; + ?> + + + + + + + + + +
+ + Attendee NameEmailCheck-in StatusCertificate Status
+ + > + + + + + + + + + + + + certificate_data)) : ?> + $attendee->certificate_data->file_path, + 'event_name' => $selected_event_title, + 'attendee_name' => $attendee_name, + 'certificate_id' => $attendee->certificate_data->certificate_id + ); + $certificate_url = $security->generate_download_token( + $attendee->certificate_data->certificate_id, + $cert_data, + 3600 // 1 hour validity + ); + } + ?> + + + Certificate Issued + + + Certificate Issued + + + No Certificate + +
+
+
+ +
+ +
+
+ +
+ + + + +
+
+ + + + + +
+ + + diff --git a/templates/certificates/template-generate-certificates.php.backup b/templates/certificates/template-generate-certificates.php.backup new file mode 100644 index 00000000..2c5d282f --- /dev/null +++ b/templates/certificates/template-generate-certificates.php.backup @@ -0,0 +1,541 @@ +check_tables(); + + if (!$tables_exist) { + echo '
Certificate database tables are not properly set up. Please contact the administrator.
'; + return; + } + +// Handle certificate generation form submission +$generation_results = null; +$errors = array(); +$success_message = ''; + +if (isset($_POST['generate_certificates']) && isset($_POST['event_id'])) { + // Verify nonce + if (!isset($_POST['hvac_certificate_nonce']) || !wp_verify_nonce($_POST['hvac_certificate_nonce'], 'hvac_generate_certificates')) { + $errors[] = 'Security verification failed. Please try again.'; + } else { + $submitted_event_id = absint($_POST['event_id']); + $selected_attendees = isset($_POST['attendee_ids']) && is_array($_POST['attendee_ids']) ? array_map('absint', $_POST['attendee_ids']) : array(); + $checked_in_only = isset($_POST['checked_in_only']) && $_POST['checked_in_only'] === 'yes'; + + // Check if any attendees were selected + if (empty($selected_attendees)) { + $errors[] = 'Please select at least one attendee to generate certificates for.'; + } else { + // Generate certificates in batch + $generation_results = $certificate_generator->generate_certificates_batch( + $submitted_event_id, + $selected_attendees, + array(), // Custom data (none for now) + $current_user_id, // Generated by current user + $checked_in_only // Only for checked-in attendees if selected + ); + + // Set success message if at least one certificate was generated + if ($generation_results['success'] > 0) { + $message_parts = array( + sprintf('Successfully generated %d certificate(s).', $generation_results['success']) + ); + + if ($generation_results['duplicate'] > 0) { + $message_parts[] = sprintf('%d duplicate(s) skipped.', $generation_results['duplicate']); + } + + if ($generation_results['not_checked_in'] > 0) { + $message_parts[] = sprintf('%d attendee(s) not checked in.', $generation_results['not_checked_in']); + } + + if ($generation_results['error'] > 0) { + $message_parts[] = sprintf('%d error(s).', $generation_results['error']); + } + + $success_message = implode(' ', $message_parts); + } elseif ($generation_results['duplicate'] > 0 && $generation_results['error'] === 0 && $generation_results['not_checked_in'] === 0) { + $success_message = sprintf( + 'No new certificates generated. %d certificate(s) already exist for the selected attendees.', + $generation_results['duplicate'] + ); + } elseif ($generation_results['not_checked_in'] > 0 && $checked_in_only) { + $success_message = sprintf( + 'No new certificates generated. %d selected attendee(s) have not been checked in.', + $generation_results['not_checked_in'] + ); + } else { + $errors[] = 'Failed to generate certificates. Please try again.'; + } + } + } +} + +// Get user's events for the event selection step using direct database query (bypassing TEC interference) +global $wpdb; + +// Build author filter +$author_filter = current_user_can('edit_others_posts') ? '' : 'AND post_author = ' . intval($current_user_id); + +// Get events directly from database +$events = $wpdb->get_results( + "SELECT ID, post_title, post_date + FROM {$wpdb->posts} + WHERE post_type = 'tribe_events' + AND post_status = 'publish' + {$author_filter} + ORDER BY post_date DESC" +); + +// Get attendees for the selected event using direct database query +$attendees = array(); +if ($event_id > 0) { + // Use direct database query to get attendees (both TEC and TPP formats) + $tec_attendees = $wpdb->get_results($wpdb->prepare( + "SELECT + p.ID as attendee_id, + p.post_parent as event_id, + COALESCE(tec_full_name.meta_value, tpp_full_name.meta_value, tickets_full_name.meta_value, 'Unknown Attendee') as holder_name, + COALESCE(tec_email.meta_value, tpp_email.meta_value, tickets_email.meta_value, tpp_attendee_email.meta_value, 'no-email@example.com') as holder_email, + COALESCE(checked_in.meta_value, '0') as check_in + FROM {$wpdb->posts} p + LEFT JOIN {$wpdb->postmeta} tec_full_name ON p.ID = tec_full_name.post_id AND tec_full_name.meta_key = '_tec_tickets_commerce_full_name' + LEFT JOIN {$wpdb->postmeta} tpp_full_name ON p.ID = tpp_full_name.post_id AND tpp_full_name.meta_key = '_tribe_tpp_full_name' + LEFT JOIN {$wpdb->postmeta} tickets_full_name ON p.ID = tickets_full_name.post_id AND tickets_full_name.meta_key = '_tribe_tickets_full_name' + LEFT JOIN {$wpdb->postmeta} tec_email ON p.ID = tec_email.post_id AND tec_email.meta_key = '_tec_tickets_commerce_email' + LEFT JOIN {$wpdb->postmeta} tpp_email ON p.ID = tpp_email.post_id AND tpp_email.meta_key = '_tribe_tpp_email' + LEFT JOIN {$wpdb->postmeta} tickets_email ON p.ID = tickets_email.post_id AND tickets_email.meta_key = '_tribe_tickets_email' + LEFT JOIN {$wpdb->postmeta} tpp_attendee_email ON p.ID = tpp_attendee_email.post_id AND tpp_attendee_email.meta_key = '_tribe_tpp_attendee_email' + LEFT JOIN {$wpdb->postmeta} checked_in ON p.ID = checked_in.post_id AND checked_in.meta_key = '_tribe_tickets_attendee_checked_in' + WHERE p.post_type IN ('tec_tc_attendee', 'tribe_tpp_attendees') + AND p.post_parent = %d + ORDER BY p.ID ASC", + $event_id + )); + + // Convert to format expected by template + foreach ($tec_attendees as $attendee) { + $attendees[] = array( + 'attendee_id' => $attendee->attendee_id, + 'event_id' => $attendee->event_id, + 'holder_name' => $attendee->holder_name, + 'holder_email' => $attendee->holder_email, + 'check_in' => intval($attendee->check_in) + ); + } +} + +// Get header and footer +get_header(); + +// Ensure certificate CSS is loaded +wp_enqueue_style( + 'hvac-certificates-style', + HVAC_CE_PLUGIN_URL . 'assets/css/hvac-certificates.css', + ['hvac-common-style'], + HVAC_CE_VERSION +); + +// Ensure dashboard CSS is loaded for proper styling +wp_enqueue_style( + 'hvac-dashboard-style', + HVAC_CE_PLUGIN_URL . 'assets/css/hvac-dashboard.css', + ['hvac-common-style'], + HVAC_CE_VERSION +); +?> + +
+
+ +
+

Generate Certificates

+ +
+ +
+

Create and manage certificates for your event attendees.

+
+ + +
+ +

+ +
+ + + + + + + +
+

Step 1: Select Event

+ + +

You don't have any events. Create an event first.

+ +
+
+ + +
+
+ +
+ + +
0 ? '' : 'style="display: none;"'; ?>> +

Step 2: Select Attendees

+ + + + + +
+
+ + + + +
+
+ +

Check this option to only generate certificates for attendees who have been marked as checked in to the event.

+
+ + +
+ 0 && !empty($attendees)) : ?> +
+ + + +
+ +
+ + + + + + + + + + + + certificate_exists($event_id, $attendee['attendee_id']); + $certificate_status = $has_certificate ? 'Certificate Issued' : 'No Certificate'; + $has_cert_class = $has_certificate ? 'hvac-has-certificate' : ''; + ?> + + + + + + + + + +
+ + AttendeeEmailStatusCertificate
+ + > + +
+
+ 0 && empty($attendees)) : ?> +

This event has no attendees.

+ +
+
+ +
+
+

Certificate Preview

+

Certificates will be generated based on your template settings.

+

A professional certificate will be generated based on the default template.

+
+
+ +
+ +
+
+
+
+ +
+

Certificate Management Tools

+

After generating certificates, you can:

+
    +
  • View all certificates on the Certificate Reports page
  • +
  • Email certificates to attendees directly from the reports page
  • +
  • Revoke certificates that were issued incorrectly
  • +
  • Download certificates in PDF format for printing or distribution
  • +
+
+
+
+ + + +Error in certificate generation: ' . esc_html($e->getMessage()) . ''; +} +?> \ No newline at end of file diff --git a/templates/communication/template-communication-schedules.php b/templates/communication/template-communication-schedules.php new file mode 100644 index 00000000..305a3e38 --- /dev/null +++ b/templates/communication/template-communication-schedules.php @@ -0,0 +1,832 @@ +ID; + +// Initialize classes +if ( ! class_exists( 'HVAC_Communication_Scheduler' ) ) { + require_once HVAC_PLUGIN_DIR . 'includes/communication/class-communication-scheduler.php'; +} + +if ( ! class_exists( 'HVAC_Communication_Schedule_Manager' ) ) { + require_once HVAC_PLUGIN_DIR . 'includes/communication/class-communication-schedule-manager.php'; +} + +if ( ! class_exists( 'HVAC_Communication_Templates' ) ) { + require_once HVAC_PLUGIN_DIR . 'includes/communication/class-communication-templates.php'; +} + +$scheduler = HVAC_Communication_Scheduler::instance(); +$schedule_manager = new HVAC_Communication_Schedule_Manager(); +$templates_manager = new HVAC_Communication_Templates(); + +// Get user's schedules +$schedules = $scheduler->get_trainer_schedules( $trainer_id ); + +// Get user's templates for dropdown +$templates = $templates_manager->get_user_templates( $trainer_id ); + +// Get user's events for dropdown +$events_query = new WP_Query( array( + 'post_type' => 'tribe_events', + 'author' => $trainer_id, + 'posts_per_page' => -1, + 'post_status' => array( 'publish', 'future', 'draft' ) +) ); + +$user_events = $events_query->posts; +?> + +
+ + +
+ +
+

Create New Schedule

+ +
+ + +
+
+ + +
+ +
+ + + Don't have templates? Create one here +
+
+ +
+
+ + +
+ +
+ + +
+
+ + + +
+ Trigger Settings + +
+
+ + +
+ +
+ +
+ + +
+
+
+ + +
+ +
+ Recurring Options (Optional) + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+

Your Schedules

+ + +
+

You haven't created any communication schedules yet.

+

Use the form above to create your first automated email schedule.

+
+ +
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Schedule NameEventTemplateTriggerStatusNext RunRunsActions
+ + + + + + + All Events + + + + + + + + + + + + N/A + + + + + / + + + + + +
+
+ +
+ + +
+

Quick Start Templates

+

Use these pre-configured schedule templates to get started quickly.

+ +
+ get_default_schedule_templates(); + foreach ( $default_templates as $template_key => $template ) : + ?> +
+

+

+
+ + +
+ +
+ +
+
+
+ + + +
+ + + + \ No newline at end of file diff --git a/templates/communication/template-communication-templates.php b/templates/communication/template-communication-templates.php new file mode 100644 index 00000000..6a5cea44 --- /dev/null +++ b/templates/communication/template-communication-templates.php @@ -0,0 +1,673 @@ +get_user_templates(); +$categories = HVAC_Communication_Templates::DEFAULT_CATEGORIES; + +// Handle first-time user setup +$current_user = wp_get_current_user(); +$has_templates = !empty($user_templates); + +// Install default templates if this is a new trainer +if (!$has_templates && in_array('hvac_trainer', $current_user->roles)) { + $install_defaults = isset($_GET['install_defaults']) ? $_GET['install_defaults'] === 'true' : false; + + if ($install_defaults) { + $templates_manager->install_default_templates(get_current_user_id()); + wp_redirect(remove_query_arg('install_defaults')); + exit; + } +} + +// Get the site title for the page title +$site_title = get_bloginfo( 'name' ); +?> + + +> + + + + <?php echo esc_html( $site_title ); ?> - <?php _e( 'Communication Templates', 'hvac-community-events' ); ?> + + + + + +> + + +
+
+
+

+

+
+ +
+ + + + +
+
+ + roles)) : ?> +
+

+

+ +
+ + + + +
+ + +
+ + + + +
+
+
+
+
+ + + +
+
+
+
+ +
+
+
+
+
+ + +
+
+ + +
+ + $label) : ?> + + 0) : ?> +
+ + +
+ + +
+ + +
+ +
+
+

+ + + + + +
+ + +

+ + +
+ + + +
+
+ +
+ + + +
+
+

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+

+

+
+
+ +
+ + +
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/templates/communication/template-manager-widget.php b/templates/communication/template-manager-widget.php new file mode 100644 index 00000000..3b35b825 --- /dev/null +++ b/templates/communication/template-manager-widget.php @@ -0,0 +1,180 @@ +get_user_templates(); +$categories = HVAC_Communication_Templates::DEFAULT_CATEGORIES; + +// Check if this is the first time user setup +$current_user = wp_get_current_user(); +$has_templates = !empty($user_templates); +?> + + + + +
+ +
+ + \ No newline at end of file diff --git a/templates/community/login-form.php b/templates/community/login-form.php new file mode 100644 index 00000000..6cc7ed22 --- /dev/null +++ b/templates/community/login-form.php @@ -0,0 +1,104 @@ + + + + \ No newline at end of file diff --git a/templates/content/trainer-account-disabled.html b/templates/content/trainer-account-disabled.html new file mode 100644 index 00000000..50877c8d --- /dev/null +++ b/templates/content/trainer-account-disabled.html @@ -0,0 +1,65 @@ + +
+
+
+

🚫

+
+ + + +

Account Access Restricted

+ + + +

Your trainer account access has been temporarily restricted. This may be due to policy violations, inactivity, or administrative review.

+ + + +
+

Common reasons for account restrictions:

+ + + +
    +
  • Terms of service violations
  • + + + +
  • Prolonged account inactivity
  • + + + +
  • Incomplete or invalid trainer credentials
  • + + + +
  • Administrative or security review
  • +
+
+ + + +

Need Help?

+ + + +

If you believe this is an error or would like to appeal this decision, please contact our support team with your account details and we'll be happy to assist you.

+ + + +
+

📧 Email: {support_email_encoded}
🕐 Hours: Monday-Friday, 9AM-5PM EST

+
+ + + + +
+
+ \ No newline at end of file diff --git a/templates/content/trainer-account-pending.html b/templates/content/trainer-account-pending.html new file mode 100644 index 00000000..ba537df3 --- /dev/null +++ b/templates/content/trainer-account-pending.html @@ -0,0 +1,59 @@ + +
+
+
+

+
+ + + +

Your Account is Pending Approval

+ + + +

Thank you for registering as an HVAC trainer! Your account has been successfully created and is currently pending approval by our team.

+ + + +

We review all new trainer applications to ensure the quality and integrity of our training network. This process typically takes 1-2 business days.

+ + + +
+

What happens next?

+ + + +
    +
  • Our team will review your application and verify your credentials
  • + + + +
  • You will receive an email notification once your account has been approved
  • + + + +
  • After approval, you can log in and start creating training events
  • + + + +
  • You will have access to all trainer tools and resources
  • +
+
+ + + +

If you have any questions about your application or need immediate assistance, please don't hesitate to contact our support team at {support_email_encoded}.

+ + + + +
+
+ \ No newline at end of file diff --git a/templates/email-attendees/template-email-attendees.php b/templates/email-attendees/template-email-attendees.php new file mode 100644 index 00000000..1421d1c2 --- /dev/null +++ b/templates/email-attendees/template-email-attendees.php @@ -0,0 +1,385 @@ +is_valid_event() ) { + wp_redirect( site_url( '/hvac-dashboard/' ) ); + exit; +} + +if ( ! $email_data->user_can_email_attendees() ) { + wp_die( __( 'You do not have permission to email attendees for this event.', 'hvac-community-events' ) ); +} + +// Get event details and attendees +$event_details = $email_data->get_event_details(); +$attendees = $email_data->get_attendees(); +$ticket_types = $email_data->get_ticket_types(); + +// Handle form submission +$email_sent = false; +$email_error = ''; +$email_success = ''; + +if ( isset( $_POST['hvac_send_email'] ) && isset( $_POST['_wpnonce'] ) && wp_verify_nonce( $_POST['_wpnonce'], 'hvac_email_attendees_' . $event_id ) ) { + + $subject = isset( $_POST['email_subject'] ) ? sanitize_text_field( $_POST['email_subject'] ) : ''; + $message = isset( $_POST['email_message'] ) ? wp_kses_post( $_POST['email_message'] ) : ''; + $cc = isset( $_POST['email_cc'] ) ? sanitize_text_field( $_POST['email_cc'] ) : ''; + + // Get selected recipients + $recipients = array(); + if ( isset( $_POST['email_attendees'] ) && is_array( $_POST['email_attendees'] ) ) { + $recipients = array_map( 'sanitize_text_field', $_POST['email_attendees'] ); + } + + // Validate and send email + if ( empty( $subject ) || empty( $message ) || empty( $recipients ) ) { + $email_error = __( 'Please fill in all required fields (subject, message, and select at least one recipient).', 'hvac-community-events' ); + } else { + $result = $email_data->send_email( $recipients, $subject, $message, $cc ); + + if ( $result['success'] ) { + $email_sent = true; + $email_success = $result['message']; + } else { + $email_error = $result['message']; + } + } +} + +// Get filtered attendees if a ticket type is selected +$selected_ticket_type = isset( $_GET['ticket_type'] ) ? sanitize_text_field( $_GET['ticket_type'] ) : ''; +if ( ! empty( $selected_ticket_type ) ) { + $attendees = $email_data->get_attendees_by_ticket_type( $selected_ticket_type ); +} + +// Get the site title for the page title +$site_title = get_bloginfo( 'name' ); +?> + + +> + + + + <?php echo esc_html( $site_title ); ?> - <?php _e( 'Email Attendees', 'hvac-community-events' ); ?> + + + + +> + + + + + + + \ No newline at end of file diff --git a/templates/event-summary/template-event-summary.php b/templates/event-summary/template-event-summary.php new file mode 100644 index 00000000..6562e3c7 --- /dev/null +++ b/templates/event-summary/template-event-summary.php @@ -0,0 +1,489 @@ +'; + echo '
'; + echo ''; + echo '
'; + get_footer(); + exit; +} + +// Get the event ID from the URL parameter +$event_id = isset( $_GET['event_id'] ) ? absint( $_GET['event_id'] ) : 0; + +// Ensure the data class is available +if ( ! class_exists( 'HVAC_Event_Summary_Data' ) ) { + // Attempt to include it if not loaded + $class_path = plugin_dir_path( __FILE__ ) . '../includes/community/class-event-summary-data.php'; + if ( file_exists( $class_path ) ) { + require_once $class_path; + } else { + // Handle error: Class not found, cannot display summary + echo "

Error: Event Summary data handler not found.

"; + return; + } +} + +// Initialize the event summary data handler +$summary_data_handler = new HVAC_Event_Summary_Data( $event_id ); + +// Check if the event is valid +if ( ! $summary_data_handler->is_valid_event() ) { + // Redirect to dashboard if the event doesn't exist + wp_safe_redirect( home_url( '/hvac-dashboard/' ) ); + exit; +} + +// Get the event post to check ownership +$event = get_post($event_id); + +// Check if the current user has permission to view this event +// Only the post author or users with edit_posts capability can view +if ($event->post_author != get_current_user_id() && !current_user_can('edit_posts')) { + get_header(); + echo '
'; + echo '
'; + echo '
You do not have permission to view this event summary.
'; + echo '

Return to Dashboard

'; + echo '
'; + get_footer(); + exit; +} + +// Fetch all the required event data +$event_details = $summary_data_handler->get_event_details(); +$venue_details = $summary_data_handler->get_event_venue_details(); +$organizer_details = $summary_data_handler->get_event_organizer_details(); +$transactions = $summary_data_handler->get_event_transactions(); + +// Calculate ticket sales summary data +$total_tickets = 0; +$total_revenue = 0; +$ticket_types = array(); + +// Process transactions data +if ( ! empty( $transactions ) ) { + foreach ( $transactions as $txn ) { + $total_tickets++; + if ( isset( $txn['price'] ) ) { + $total_revenue += floatval( $txn['price'] ); + } + + // Count ticket types + $ticket_type = $txn['ticket_type_name'] ?? 'Unknown'; + if ( isset( $ticket_types[$ticket_type] ) ) { + $ticket_types[$ticket_type]['count']++; + if ( isset( $txn['price'] ) ) { + $ticket_types[$ticket_type]['revenue'] += floatval( $txn['price'] ); + } + } else { + $ticket_types[$ticket_type] = array( + 'count' => 1, + 'revenue' => isset( $txn['price'] ) ? floatval( $txn['price'] ) : 0, + ); + } + } +} + +// Start the template +get_header(); +?> + +
+
+ + +
+

- Summary

+
+ Dashboard + Edit Event'; + } + + // View public event page + echo 'View Public Page'; + + // Email attendees link + if ( current_user_can( 'edit_post', $event_id ) ) { + $email_url = add_query_arg( 'event_id', $event_id, home_url( '/email-attendees/' ) ); + echo 'Email Attendees'; + + // Certificate generation link + $certificate_url = add_query_arg( 'event_id', $event_id, home_url( '/generate-certificates/' ) ); + echo 'Generate Certificates'; + } + ?> +
+
+ + +
+

Event Overview

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Date & Time: + +
Status:
Cost:
Venue: + + +
+ +
Organizer: + + +
+ +
+
+
+
+ + +
+

Event Statistics

+
+ +
+
+

Total Tickets

+

+
+
+ + +
+
+

Total Revenue

+

$

+
+
+ + + $data ) : ?> +
+
+

+

+ $ +
+
+ +
+
+ + +
+

Ticket Sales & Attendees

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
AttendeeEmailTicket TypePriceOrder IDChecked InCertificate
+ + + + + + + + $ + + + + + + + + + $event_id, + 'attendee_id' => $txn['attendee_id'] + ), + home_url('/generate-certificates/') + ); + echo ' Generate'; + } elseif ($certificate_status == 'Generated' || $certificate_status == 'Sent') { + // If certificate exists and is active, show view/email actions + echo ' View'; + echo ' '; + echo ' Revoke'; + } + ?> +
+
+ +

No ticket sales or attendees found for this event.

+ +
+ + +
+

Event Description

+
+
+ +
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/templates/status/trainer-account-disabled.php b/templates/status/trainer-account-disabled.php new file mode 100644 index 00000000..4c061630 --- /dev/null +++ b/templates/status/trainer-account-disabled.php @@ -0,0 +1,36 @@ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ render_navigation(); + } + + // Render breadcrumbs + if (class_exists('HVAC_Breadcrumbs')) { + $breadcrumbs = new HVAC_Breadcrumbs(); + echo $breadcrumbs->render_breadcrumbs(); + } + ?> +
\ No newline at end of file