Home / Admin / NEXTSTEP SHORTCODE
Duplicate Snippet

Embed Snippet on Your Site

NEXTSTEP SHORTCODE

ismail daugherty PRO
<10
Code Preview
php
<?php
/**
 * Compliance Calendar - Cron Jobs (Overdue Detection)
 * Checks for overdue compliance items and sends notifications via GHL webhook
 * 
 * @package     BHA_Portal
 * @subpackage  Compliance_Calendar
 * @version     6.1
 * 
 * WPCODE SETTINGS:
 * - Location: Run Everywhere
 * - Auto Insert: Site Wide Header
 * - Code Type: PHP Snippet
 * - Priority: 10
 */
// Prevent direct access
if (!defined('ABSPATH')) {
    exit;
}
/**
 * =============================================================================
 * CONFIGURATION
 * =============================================================================
 */
/**
 * GoHighLevel Webhook URL for overdue notifications
 * 
 * INSTRUCTIONS:
 * 1. Log into GoHighLevel
 * 2. Go to Automation > Workflows
 * 3. Create a webhook trigger or use existing one
 * 4. Copy the webhook URL
 * 5. Replace the placeholder below with your actual URL
 */
if (!defined('BHA_GHL_WEBHOOK_URL')) {
    define('BHA_GHL_WEBHOOK_URL', 'YOUR_GHL_WEBHOOK_URL_HERE'); // <-- UPDATE THIS
}
/**
 * Enable/disable email notifications as backup
 * Set to true to also send email to admin when items are overdue
 */
if (!defined('BHA_COMPLIANCE_EMAIL_BACKUP')) {
    define('BHA_COMPLIANCE_EMAIL_BACKUP', false);
}
/**
 * =============================================================================
 * CRON SCHEDULE REGISTRATION
 * =============================================================================
 */
/**
 * Register custom cron schedule for twice daily checks
 * 
 * @since 1.0.0
 * @param array $schedules Existing schedules
 * @return array Modified schedules
 */
function bha_compliance_cron_schedules($schedules) {
    
    $schedules['bha_twice_daily'] = array(
        'interval' => 43200, // 12 hours
        'display'  => __('Twice Daily (Compliance Check)', 'bha-portal'),
    );
    
    return $schedules;
}
add_filter('cron_schedules', 'bha_compliance_cron_schedules');
/**
 * Schedule the daily compliance check cron job
 * Runs at 2 AM and 2 PM server time
 * 
 * @since 1.0.0
 * @return void
 */
function bha_schedule_compliance_cron() {
    
    if (!wp_next_scheduled('bha_daily_compliance_check')) {
        // Schedule for 2 AM server time
        $timestamp = strtotime('today 2:00am');
        
        // If 2 AM has passed today, schedule for tomorrow
        if ($timestamp < time()) {
            $timestamp = strtotime('tomorrow 2:00am');
        }
        
        wp_schedule_event($timestamp, 'bha_twice_daily', 'bha_daily_compliance_check');
        
        bha_compliance_log('Cron job scheduled for ' . wp_date('Y-m-d H:i:s', $timestamp), 'success');
    }
}
add_action('wp', 'bha_schedule_compliance_cron');
/**
 * Unschedule cron on deactivation (for plugin context)
 * Call this when deactivating to clean up scheduled events
 * 
 * @since 1.0.0
 * @return void
 */
function bha_unschedule_compliance_cron() {
    wp_clear_scheduled_hook('bha_daily_compliance_check');
}
/**
 * =============================================================================
 * MAIN CRON FUNCTION
 * =============================================================================
 */
/**
 * Main function that runs on cron schedule
 * Checks all compliance trackers for overdue items
 * 
 * @since 1.0.0
 * @return void
 */
function bha_check_overdue_compliance() {
    
    bha_compliance_log('Starting daily compliance check...', 'info');
    
    // Use WordPress timezone-aware date
    $current_year = (int) wp_date('Y');
    
    // Get all compliance trackers for current year
    $trackers = get_posts(array(
        'post_type'      => 'compliance_tracker',
        'post_status'    => 'publish',
        'meta_query'     => array(
            array(
                'key'   => 'tracking_year',
                'value' => $current_year,
            ),
        ),
        'posts_per_page' => -1,
    ));
    
    if (empty($trackers)) {
        bha_compliance_log('No trackers found for ' . $current_year, 'info');
        return;
    }
    
    bha_compliance_log('Checking ' . count($trackers) . ' trackers...', 'info');
    
    $total_overdue = 0;
    
    foreach ($trackers as $tracker) {
        $overdue_items = bha_get_overdue_items($tracker->ID);
        
        if (!empty($overdue_items)) {
            $total_overdue += count($overdue_items);
            bha_send_overdue_notification($tracker->ID, $overdue_items);
        }
    }
    
    bha_compliance_log("Compliance check complete. Total overdue items: {$total_overdue}", 'success');
}
add_action('bha_daily_compliance_check', 'bha_check_overdue_compliance');
/**
 * =============================================================================
 * OVERDUE DETECTION FUNCTIONS
 * =============================================================================
 */
/**
 * Get all overdue items for a compliance tracker
 * 
 * @since 1.0.0
 * @param int $post_id Tracker post ID
 * @return array Array of overdue items with details
 */
function bha_get_overdue_items($post_id) {
    
    $post_id = absint($post_id);
    $overdue_items = array();
    
    // Use WordPress timezone-aware date
    $today = wp_date('Y-m-d');
    $current_month = (int) wp_date('n'); // 1-12
    $current_quarter = (int) ceil($current_month / 3);
    
    // Get due date settings
    $monthly_due_day = absint(get_field('monthly_due_day', $post_id)) ?: 15;
    $tracking_year = absint(get_field('tracking_year', $post_id)) ?: (int) wp_date('Y');
    
    // Monthly items configuration
    $monthly_items = array(
        'emergency_drill'        => 'Emergency Drill',
        'budget_projected'       => 'Budget Projected',
        'budget_actual'          => 'Budget Actual',
        'vehicle_inspection'     => 'Vehicle Inspection',
        'transportation_mileage' => 'Transportation Mileage',
        'staff_meeting'          => 'Staff Meeting',
        'hs_inspection'          => 'H&S Inspection',
        'clinical_supervision'   => 'Clinical Supervision',
        'grievances'             => 'Grievances',
        'incident_reports'       => 'Incident Reports',
        'client_accommodation'   => 'Client Accommodation',
        'staff_accommodation'    => 'Staff Accommodation',
    );
    
    $months = array(
        1  => 'jan', 2  => 'feb', 3  => 'mar', 4  => 'apr',
        5  => 'may', 6  => 'jun', 7  => 'jul', 8  => 'aug',
        9  => 'sep', 10 => 'oct', 11 => 'nov', 12 => 'dec',
    );
    
    // Check monthly items (only check past months + current if past due day)
    foreach ($monthly_items as $item_key => $item_label) {
        
        for ($month_num = 1; $month_num <= 12; $month_num++) {
            
            $month_abbr = $months[$month_num];
            
            // Calculate due date for this month
            $due_date = sprintf('%d-%02d-%02d', $tracking_year, $month_num, $monthly_due_day);
            
            // Skip future due dates
            if ($due_date > $today) {
                continue;
            }
            
            // Check if item is complete
            $is_complete = get_field("{$item_key}_{$month_abbr}_complete", $post_id);
            
            if (!$is_complete) {
                $overdue_items[] = array(
                    'item_key'   => $item_key,
                    'item_label' => $item_label,
                    'frequency'  => 'monthly',
                    'period'     => ucfirst($month_abbr),
                    'due_date'   => $due_date,
                    'days_overdue' => bha_calculate_days_overdue($due_date),
                );
            }
        }
    }
    
    // Quarterly items
    $quarterly_items = array(
        'billing_audit'          => 'Billing Audit',
        'clinical_record_review' => 'Clinical Record Review',
    );
    
    $quarter_due_dates = array(
        1 => get_field('q1_due_date', $post_id) ?: sprintf('%d-03-31', $tracking_year),
        2 => get_field('q2_due_date', $post_id) ?: sprintf('%d-06-30', $tracking_year),
        3 => get_field('q3_due_date', $post_id) ?: sprintf('%d-09-30', $tracking_year),
        4 => get_field('q4_due_date', $post_id) ?: sprintf('%d-12-31', $tracking_year),
    );
    
    foreach ($quarterly_items as $item_key => $item_label) {
        
        for ($q = 1; $q <= 4; $q++) {
            
            $due_date = $quarter_due_dates[$q];
            
            // Skip future due dates
            if ($due_date > $today) {
                continue;
            }
            
            // Check if item is complete
            $is_complete = get_field("{$item_key}_q{$q}_complete", $post_id);
            
            if (!$is_complete) {
                $overdue_items[] = array(
                    'item_key'   => $item_key,
                    'item_label' => $item_label,
                    'frequency'  => 'quarterly',
                    'period'     => "Q{$q}",
                    'due_date'   => $due_date,
                    'days_overdue' => bha_calculate_days_overdue($due_date),
                );
            }
        }
    }
    
    // Annual items
    $annual_items = array(
        'strategic_meeting'    => 'Strategic Meeting',
        'satisfaction_surveys' => 'Satisfaction Surveys',
    );
    
    $annual_due_date = get_field('annual_due_date', $post_id) ?: sprintf('%d-12-15', $tracking_year);
    
    foreach ($annual_items as $item_key => $item_label) {
        
        // Skip if not yet due
        if ($annual_due_date > $today) {
            continue;
        }
        
        $is_complete = get_field("{$item_key}_complete", $post_id);
        
        if (!$is_complete) {
            $overdue_items[] = array(
                'item_key'   => $item_key,
                'item_label' => $item_label,
                'frequency'  => 'annual',
                'period'     => 'Annual',
                'due_date'   => $annual_due_date,
                'days_overdue' => bha_calculate_days_overdue($annual_due_date),
            );
        }
    }
    
    return $overdue_items;
}
/**
 * Calculate days overdue from due date
 * 
 * @since 1.0.0
 * @param string $due_date Due date in Y-m-d format
 * @return int Days overdue (0 if not overdue)
 */
function bha_calculate_days_overdue($due_date) {
    
    $due = new DateTime($due_date);
    $today = new DateTime('today');
    
    if ($today <= $due) {
        return 0;
    }
    
    return (int) $today->diff($due)->days;
}
/**
 * =============================================================================
 * NOTIFICATION FUNCTIONS
 * =============================================================================
 */
/**
 * Send overdue notification via GHL webhook
 * 
 * @since 1.0.0
 * @param int $post_id Tracker post ID
 * @param array $overdue_items Array of overdue items
 * @return bool True on success, false on failure
 */
function bha_send_overdue_notification($post_id, $overdue_items) {
    
    $post_id = absint($post_id);
    
    // Get organization info - sanitize all data from database
    $group_id = absint(get_field('group_id', $post_id));
    $organization_name = sanitize_text_field(get_field('organization_name', $post_id));
    $tracking_year = absint(get_field('tracking_year', $post_id));
    $primary_user_id = absint(get_field('wordpress_user_id', $post_id));
    
    // Get primary contact email with validation
    $contact_email = '';
    if ($primary_user_id > 0) {
        $user = get_userdata($primary_user_id);
        if ($user && is_email($user->user_email)) {
            $contact_email = sanitize_email($user->user_email);
        }
    }
    
    // Build summary of overdue items
    $overdue_summary = array();
    foreach ($overdue_items as $item) {
        $overdue_summary[] = sprintf(
            '%s (%s) - %d days overdue',
            $item['item_label'],
            $item['period'],
            $item['days_overdue']
        );
    }
    
    // Prepare webhook payload
    $payload = array(
        'event_type'         => 'compliance_overdue',
        'timestamp'          => wp_date('c'), // ISO 8601 format with timezone
        'group_id'           => $group_id,
        'organization_name'  => $organization_name,
        'tracking_year'      => $tracking_year,
        'contact_email'      => $contact_email,
        'total_overdue'      => count($overdue_items),
        'overdue_items'      => $overdue_items,
        'overdue_summary'    => implode("\n", $overdue_summary),
        'tracker_edit_url'   => admin_url("post.php?post={$post_id}&action=edit"),
    );
    
    // Send to GHL webhook
    $webhook_url = BHA_GHL_WEBHOOK_URL;
    
    // Validate webhook URL
    if ($webhook_url && $webhook_url !== 'YOUR_GHL_WEBHOOK_URL_HERE') {
        
        // Sanitize and validate URL
        $webhook_url = esc_url_raw($webhook_url);
        
        if (empty($webhook_url) || !wp_http_validate_url($webhook_url)) {
            bha_compliance_log(
                "Invalid webhook URL configured for group {$group_id}",
                'error'
            );
            return false;
        }
        
        $response = wp_remote_post($webhook_url, array(
            'method'    => 'POST',
            'timeout'   => 30,
            'headers'   => array(
                'Content-Type' => 'application/json',
            ),
            'body'      => wp_json_encode($payload),
        ));
        
        if (is_wp_error($response)) {
            bha_compliance_log(
                "Webhook failed for group {$group_id}: " . $response->get_error_message(),
                'error'
            );
            
            // Fallback to email if enabled
            if (BHA_COMPLIANCE_EMAIL_BACKUP) {
                bha_send_overdue_email($organization_name, $contact_email, $overdue_summary);
            }
            
            return false;
        }
        
        $response_code = wp_remote_retrieve_response_code($response);
        
        if ($response_code >= 200 && $response_code < 300) {
            bha_compliance_log(
                "Webhook sent for group {$group_id}: {$organization_name} - " . count($overdue_items) . " overdue items",
                'success'
            );
            return true;
        } else {
            bha_compliance_log(
                "Webhook returned {$response_code} for group {$group_id}",
                'warning'
            );
            return false;
        }
        
    } else {
        // No webhook configured - log and optionally email
        bha_compliance_log(
            "No webhook configured. Group {$group_id} ({$organization_name}) has " . count($overdue_items) . " overdue items",
            'warning'
        );
        
        if (BHA_COMPLIANCE_EMAIL_BACKUP) {
            bha_send_overdue_email($organization_name, $contact_email, $overdue_summary);
        }
        
        return false;
    }
}
/**
 * Send overdue notification via email (backup method)
 * 
 * @since 1.0.0
 * @param string $organization_name Organization name
 * @param string $contact_email Contact email
 * @param array $overdue_summary Array of overdue summary strings
 * @return bool True on success
 */
function bha_send_overdue_email($organization_name, $contact_email, $overdue_summary) {
    
    // Sanitize inputs
    $organization_name = sanitize_text_field($organization_name);
    $contact_email = sanitize_email($contact_email);
    $admin_email = sanitize_email(get_option('admin_email'));
    $site_name = sanitize_text_field(get_bloginfo('name'));
    
    // Build recipient list
    $to = $admin_email;
    if ($contact_email && is_email($contact_email) && $contact_email !== $admin_email) {
        $to .= ',' . $contact_email;
    }
    
    // Sanitize summary items
    $sanitized_summary = array_map('sanitize_text_field', $overdue_summary);
    
    $subject = sprintf(
        /* translators: 1: Site name, 2: Organization name */
        __('[%1$s] Compliance Items Overdue - %2$s', 'bha-portal'),
        $site_name,
        $organization_name
    );
    
    $message = sprintf(
        /* translators: %s: Organization name */
        __('The following compliance items are overdue for %s:', 'bha-portal'),
        $organization_name
    );
    $message .= "\n\n";
    $message .= implode("\n", $sanitized_summary);
    $message .= "\n\n";
    $message .= __('Please log in to complete these items as soon as possible.', 'bha-portal');
    
    return wp_mail($to, $subject, $message);
}
/**
 * =============================================================================
 * MANUAL TRIGGER & ADMIN FUNCTIONS
 * =============================================================================
 */
/**
 * Manual trigger for compliance check
 * Visit: yoursite.com/wp-admin/?run_compliance_check&_wpnonce=xxx
 * 
 * @since 1.0.0
 * @return void
 */
function bha_manual_compliance_check() {
    
    // Must be admin area
    if (!is_admin()) {
        return;
    }
    
    // Check if our parameter is set
    if (!isset($_GET['run_compliance_check'])) {
        return;
    }
    
    // Capability check FIRST (per WordPress docs: always check capabilities)
    if (!current_user_can('manage_options')) {
        wp_die(
            esc_html__('You do not have permission to perform this action.', 'bha-portal'),
            esc_html__('Permission Denied', 'bha-portal'),
            array('response' => 403)
        );
    }
    
    // Nonce verification (per WordPress docs: use nonces for all admin actions)
    // Sanitize nonce value before verification as recommended
    $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';
    
    if (!wp_verify_nonce($nonce, 'bha_run_compliance_check')) {
        wp_die(
            esc_html__('Security check failed. Please try again.', 'bha-portal'),
            esc_html__('Security Error', 'bha-portal'),
            array('response' => 403)
        );
    }
    
    echo '<h2>' . esc_html__('Running Manual Compliance Check...', 'bha-portal') . '</h2>';
    echo '<pre>';
    
    // Temporarily enable logging output
    add_action('bha_compliance_log_output', function($message) {
        echo esc_html($message) . "\n";
    });
    
    bha_check_overdue_compliance();
    
    echo '</pre>';
    echo '<p><strong>' . esc_html__('Check complete!', 'bha-portal') . '</strong></p>';
    echo '<p><a href="' . esc_url(admin_url('edit.php?post_type=compliance_tracker')) . '">' . esc_html__('View Compliance Trackers', 'bha-portal') . '</a></p>';
    
    // Show cron status
    $next_scheduled = wp_next_scheduled('bha_daily_compliance_check');
    if ($next_scheduled) {
        echo '<p>' . esc_html__('Next scheduled check:', 'bha-portal') . ' ' . esc_html(wp_date('Y-m-d H:i:s', $next_scheduled)) . '</p>';
    } else {
        echo '<p>' . esc_html__('⚠️ No cron job scheduled. Visit any page to schedule it.', 'bha-portal') . '</p>';
    }
    
    wp_die();
}
add_action('admin_init', 'bha_manual_compliance_check');
/**
 * Check for specific organization's overdue items
 * 
 * @since 1.0.0
 * @param int $group_id BuddyBoss group ID
 * @param int|null $year Tracking year (defaults to current)
 * @return array Array of overdue items
 */
function bha_get_organization_overdue($group_id, $year = null) {
    
    $group_id = absint($group_id);
    
    if ($group_id < 1) {
        return array();
    }
    
    $tracker = bha_get_compliance_tracker($group_id, $year);
    
    if (!$tracker) {
        return array();
    }
    
    return bha_get_overdue_items($tracker->ID);
}
/**
 * Get compliance status summary for an organization
 * 
 * @since 1.0.0
 * @param int $group_id BuddyBoss group ID
 * @param int|null $year Tracking year
 * @return array Status summary with counts and percentages
 */
function bha_get_compliance_status_summary($group_id, $year = null) {
    
    $group_id = absint($group_id);
    
    if ($group_id < 1) {
        return array(
            'error' => 'Invalid group_id',
        );
    }
    
    $tracker = bha_get_compliance_tracker($group_id, $year);
    
    if (!$tracker) {
        return array(
            'error' => 'No tracker found',
        );
    }
    
    $post_id = $tracker->ID;
    
    // Count totals
    $total_items = 0;
    $completed_items = 0;
    $overdue_items = count(bha_get_overdue_items($post_id));
    
    // Monthly items: 12 items × 12 months = 144 possible
    $months = array('jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec');
    $monthly_items = array(
        'emergency_drill', 'budget_projected', 'budget_actual', 'vehicle_inspection',
        'transportation_mileage', 'staff_meeting', 'hs_inspection', 'clinical_supervision',
        'grievances', 'incident_reports', 'client_accommodation', 'staff_accommodation',
    );
    
    foreach ($months as $month) {
        foreach ($monthly_items as $item) {
            $total_items++;
            if (get_field("{$item}_{$month}_complete", $post_id)) {
                $completed_items++;
            }
        }
    }
    
    // Quarterly: 2 items × 4 quarters = 8 possible
    $quarterly_items = array('billing_audit', 'clinical_record_review');
    for ($q = 1; $q <= 4; $q++) {
        foreach ($quarterly_items as $item) {
            $total_items++;
            if (get_field("{$item}_q{$q}_complete", $post_id)) {
                $completed_items++;
            }
        }
    }
    
    // Annual: 2 items
    $annual_items = array('strategic_meeting', 'satisfaction_surveys');
    foreach ($annual_items as $item) {
        $total_items++;
        if (get_field("{$item}_complete", $post_id)) {
            $completed_items++;
        }
    }
    
    $percentage = $total_items > 0 ? round(($completed_items / $total_items) * 100, 1) : 0;
    
    return array(
        'group_id'          => $group_id,
        'organization_name' => sanitize_text_field(get_field('organization_name', $post_id)),
        'tracking_year'     => absint(get_field('tracking_year', $post_id)),
        'total_items'       => $total_items,
        'completed_items'   => $completed_items,
        'pending_items'     => $total_items - $completed_items,
        'overdue_items'     => $overdue_items,
        'percentage'        => $percentage,
        'status'            => $percentage >= 80 ? 'good' : ($percentage >= 50 ? 'warning' : 'critical'),
    );
}
/**
 * Calculate compliance stats for admin column display
 * This function is called by the CPT admin columns
 * 
 * @since 1.0.0
 * @param int $post_id Tracker post ID
 * @return array Stats array with percentage
 */
function bha_calculate_compliance_stats($post_id) {
    
    $post_id = absint($post_id);
    $group_id = absint(get_field('group_id', $post_id));
    
    if ($group_id < 1) {
        return array('percentage' => 0);
    }
    
    $summary = bha_get_compliance_status_summary($group_id, get_field('tracking_year', $post_id));
    
    return array(
        'percentage' => isset($summary['percentage']) ? $summary['percentage'] : 0,
    );
}
/**
 * =============================================================================
 * CRON STATUS CHECK
 * =============================================================================
 */
/**
 * Admin notice if cron is not scheduled
 * 
 * @since 1.0.0
 * @return void
 */
function bha_compliance_cron_admin_notice() {
    
    if (!current_user_can('manage_options')) {
        return;
    }
    
    $screen = get_current_screen();
    if (!$screen || $screen->post_type !== 'compliance_tracker') {
        return;
    }
    
    $next_scheduled = wp_next_scheduled('bha_daily_compliance_check');
    
    if (!$next_scheduled) {
        $manual_check_url = wp_nonce_url(
            admin_url('?run_compliance_check'),
            'bha_run_compliance_check'
        );
        
        echo '<div class="notice notice-warning"><p>';
        echo '<strong>' . esc_html__('Compliance Calendar:', 'bha-portal') . '</strong> ';
        echo esc_html__('Automated overdue checking is not scheduled.', 'bha-portal') . ' ';
        echo '<a href="' . esc_url($manual_check_url) . '">' . esc_html__('Run manual check', 'bha-portal') . '</a>';
        echo '</p></div>';
    }
}
add_action('admin_notices', 'bha_compliance_cron_admin_notice');
/**
 * =============================================================================
 * LOGGING (Shared with form-hooks if not already defined)
 * =============================================================================
 */
if (!function_exists('bha_compliance_log')) {
    /**
     * Log compliance-related messages
     * 
     * @since 1.0.0
     * @param string $message Log message
     * @param string $level Log level (success, warning, error, info)
     * @return void
     */
    function bha_compliance_log($message, $level = 'info') {
        
        // Allow output for manual triggers
        do_action('bha_compliance_log_output', "[{$level}] {$message}");
        
        if (!defined('WP_DEBUG') || !WP_DEBUG) {
            return;
        }
        
        $prefix = '[BHA Compliance]';
        
        switch ($level) {
            case 'success':
                $prefix .= ' ✓';
                break;
            case 'warning':
                $prefix .= ' ⚠';
                break;
            case 'error':
                $prefix .= ' ✗';
                break;
            default:
                $prefix .= ' ℹ';
        }
        
        error_log("{$prefix} {$message}");
    }
}

Comments

Add a Comment