upskill-event-manager/includes/communication/class-communication-schedule-manager.php
bengizmo 37f4180e1c feat: Add massive missing plugin infrastructure to repository
🚨 CRITICAL: Fixed deployment blockers by adding missing core directories:

**Community System (CRITICAL)**
- includes/community/ - Login_Handler and all community classes
- templates/community/ - Community login forms

**Certificate System (CRITICAL)**
- includes/certificates/ - 8+ certificate classes and handlers
- templates/certificates/ - Certificate reports and generation templates

**Core Individual Classes (CRITICAL)**
- includes/class-hvac-event-summary.php
- includes/class-hvac-trainer-profile-manager.php
- includes/class-hvac-master-dashboard-data.php
- Plus 40+ other individual HVAC classes

**Major Feature Systems (HIGH)**
- includes/database/ - Training leads database tables
- includes/find-trainer/ - Find trainer directory and MapGeo integration
- includes/google-sheets/ - Google Sheets integration system
- includes/zoho/ - Complete Zoho CRM integration
- includes/communication/ - Communication templates system

**Template Infrastructure**
- templates/attendee/, templates/email-attendees/
- templates/event-summary/, templates/status/
- templates/template-parts/ - Shared template components

**Impact:**
- 70+ files added covering 10+ missing directories
- Resolves ALL deployment blockers and feature breakdowns
- Plugin activation should now work correctly
- Multi-machine deployment fully supported

🔧 Generated with Claude Code

Co-Authored-By: Ben Reed <ben@tealmaker.com>
2025-08-11 13:30:11 -03:00

603 lines
No EOL
21 KiB
PHP

<?php
/**
* HVAC Community Events - Communication Schedule Manager
*
* Handles CRUD operations for communication schedules.
* Manages database interactions and schedule validation.
*
* @package HVAC_Community_Events
* @subpackage Communication
* @version 1.0.0
*/
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class HVAC_Communication_Schedule_Manager
*
* Manages communication schedule database operations.
*/
class HVAC_Communication_Schedule_Manager {
/**
* Database table names
*/
private $schedules_table;
private $logs_table;
private $tracking_table;
/**
* Constructor
*/
public function __construct() {
global $wpdb;
$this->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;
}
}