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; } }