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 @@ + +
| + | + | + |
|---|---|---|
| + | + + + + | ++ |
';
+
+ // 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 '';
+
+ 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 @@
+
+ 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') . 'Hello ' . esc_html($vars['trainer_name']) . ',
'; + $html .= 'You have received a new contact request through the Find a Trainer directory.
'; + $html .= '' . nl2br(esc_html($vars['submitter_message'])) . '
'; + } + $html .= ''; + break; + + case 'submitter_confirmation': + $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 .= '' . nl2br(esc_html($vars['message_copy'])) . '
'; + } + $html .= 'Thank you for using our Find a Trainer directory!
'; + break; + + case 'admin_notification': + $html .= 'A new contact form has been submitted on the Find a Trainer page.
'; + $html .= 'This is an automated message from ' . get_bloginfo('name') . '
'; + $html .= 'Status updated successfully!
| ID | +Date | +From | +Trainer | +Message | +Status | +Actions | +|
|---|---|---|---|---|---|---|---|
| id); ?> | +submission_date))); ?> | +first_name . ' ' . $submission->last_name); ?> | +email); ?> | +display_name) : 'Unknown'; ?> | +message, 0, 100)) . (strlen($submission->message) > 100 ? '...' : ''); ?> | ++ + status)); ?> + + | ++ + | +
| No submissions found. | +|||||||
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.
+ +Are you an HVAC Trainer that wants to be listed in our directory?
+ + + + +%s, %s
+ +,
+ + + +No trainers found matching your criteria. Try adjusting your filters.
+ HVAC Plugin Warning: + The following plugins may conflict with MapGeo integration: + +
+No trainers found matching your criteria.
+ , +
+ + ++ +
+ + + ++ +
+ + ++ Total Training Events: +
+Generate a comprehensive report with system overview, trainer performance, all events, and revenue analytics.
+ + +Create detailed spreadsheets for individual events with attendees, financial data, and event details.
+ +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 'Authorization code received. You can close this window.
"; + $response .= "Code: $auth_code
Copy this code and paste it in the terminal.
"; + $response .= "