| |
| <?php
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| if (!defined('ABSPATH')) {
|
| exit;
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| if (!defined('BHA_GHL_WEBHOOK_URL')) {
|
| define('BHA_GHL_WEBHOOK_URL', 'YOUR_GHL_WEBHOOK_URL_HERE');
|
| }
|
|
|
|
|
|
|
|
|
|
|
| if (!defined('BHA_COMPLIANCE_EMAIL_BACKUP')) {
|
| define('BHA_COMPLIANCE_EMAIL_BACKUP', false);
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| function bha_compliance_cron_schedules($schedules) {
|
|
|
| $schedules['bha_twice_daily'] = array(
|
| 'interval' => 43200,
|
| 'display' => __('Twice Daily (Compliance Check)', 'bha-portal'),
|
| );
|
|
|
| return $schedules;
|
| }
|
| add_filter('cron_schedules', 'bha_compliance_cron_schedules');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| function bha_schedule_compliance_cron() {
|
|
|
| if (!wp_next_scheduled('bha_daily_compliance_check')) {
|
|
|
| $timestamp = strtotime('today 2:00am');
|
|
|
|
|
| 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');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| function bha_unschedule_compliance_cron() {
|
| wp_clear_scheduled_hook('bha_daily_compliance_check');
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| function bha_check_overdue_compliance() {
|
|
|
| bha_compliance_log('Starting daily compliance check...', 'info');
|
|
|
|
|
| $current_year = (int) wp_date('Y');
|
|
|
|
|
| $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');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| function bha_get_overdue_items($post_id) {
|
|
|
| $post_id = absint($post_id);
|
| $overdue_items = array();
|
|
|
|
|
| $today = wp_date('Y-m-d');
|
| $current_month = (int) wp_date('n');
|
| $current_quarter = (int) ceil($current_month / 3);
|
|
|
|
|
| $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 = 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',
|
| );
|
|
|
|
|
| foreach ($monthly_items as $item_key => $item_label) {
|
|
|
| for ($month_num = 1; $month_num <= 12; $month_num++) {
|
|
|
| $month_abbr = $months[$month_num];
|
|
|
|
|
| $due_date = sprintf('%d-%02d-%02d', $tracking_year, $month_num, $monthly_due_day);
|
|
|
|
|
| if ($due_date > $today) {
|
| continue;
|
| }
|
|
|
|
|
| $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 = 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];
|
|
|
|
|
| if ($due_date > $today) {
|
| continue;
|
| }
|
|
|
|
|
| $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 = 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) {
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| function bha_send_overdue_notification($post_id, $overdue_items) {
|
|
|
| $post_id = absint($post_id);
|
|
|
|
|
| $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));
|
|
|
|
|
| $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);
|
| }
|
| }
|
|
|
|
|
| $overdue_summary = array();
|
| foreach ($overdue_items as $item) {
|
| $overdue_summary[] = sprintf(
|
| '%s (%s) - %d days overdue',
|
| $item['item_label'],
|
| $item['period'],
|
| $item['days_overdue']
|
| );
|
| }
|
|
|
|
|
| $payload = array(
|
| 'event_type' => 'compliance_overdue',
|
| 'timestamp' => wp_date('c'),
|
| '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"),
|
| );
|
|
|
|
|
| $webhook_url = BHA_GHL_WEBHOOK_URL;
|
|
|
|
|
| if ($webhook_url && $webhook_url !== 'YOUR_GHL_WEBHOOK_URL_HERE') {
|
|
|
|
|
| $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'
|
| );
|
|
|
|
|
| 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 {
|
|
|
| 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;
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| function bha_send_overdue_email($organization_name, $contact_email, $overdue_summary) {
|
|
|
|
|
| $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'));
|
|
|
|
|
| $to = $admin_email;
|
| if ($contact_email && is_email($contact_email) && $contact_email !== $admin_email) {
|
| $to .= ',' . $contact_email;
|
| }
|
|
|
|
|
| $sanitized_summary = array_map('sanitize_text_field', $overdue_summary);
|
|
|
| $subject = sprintf(
|
|
|
| __('[%1$s] Compliance Items Overdue - %2$s', 'bha-portal'),
|
| $site_name,
|
| $organization_name
|
| );
|
|
|
| $message = sprintf(
|
|
|
| __('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);
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| function bha_manual_compliance_check() {
|
|
|
|
|
| if (!is_admin()) {
|
| return;
|
| }
|
|
|
|
|
| if (!isset($_GET['run_compliance_check'])) {
|
| return;
|
| }
|
|
|
|
|
| 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 = 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>';
|
|
|
|
|
| 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>';
|
|
|
|
|
| $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');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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);
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|
|
|
|
|
| $total_items = 0;
|
| $completed_items = 0;
|
| $overdue_items = count(bha_get_overdue_items($post_id));
|
|
|
|
|
| $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_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_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'),
|
| );
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| );
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| if (!function_exists('bha_compliance_log')) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| function bha_compliance_log($message, $level = 'info') {
|
|
|
|
|
| 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