/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ db_handler = $db_handler; $this->restriction_model = new Care_Booking_Restriction_Model(); $this->cache_manager = new Care_Booking_Cache_Manager(); $this->init_hooks(); } /** * Initialize WordPress hooks */ private function init_hooks() { // Enhanced KiviCare filter hooks with multiple compatibility points // Priority 10 for standard filtering, Priority 5 for early filtering add_filter('kc_get_doctors_for_booking', [$this, 'filter_doctors'], 10, 1); add_filter('kivicare_doctors_list', [$this, 'filter_doctors'], 10, 1); add_filter('kivicare_get_doctors', [$this, 'filter_doctors'], 10, 1); // Service filtering with multiple hook points add_filter('kc_get_services_by_doctor', [$this, 'filter_services'], 10, 2); add_filter('kivicare_services_list', [$this, 'filter_services'], 10, 2); add_filter('kivicare_get_services', [$this, 'filter_services'], 10, 2); // Enhanced CSS injection with optimized priority add_action('wp_head', [$this, 'inject_restriction_css'], 15); // Frontend JavaScript for graceful degradation add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_scripts'], 10); // Frontend CSS for base styles add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_styles'], 10); // KiviCare 3.0+ REST API hooks add_filter('rest_pre_serve_request', [$this, 'filter_rest_api_response'], 10, 4); // Admin bar integration (optional) if (is_admin_bar_showing()) { add_action('admin_bar_menu', [$this, 'add_admin_bar_menu'], 100); } } /** * Filter KiviCare doctors list to remove blocked doctors * * @param array $doctors Array of doctors from KiviCare * @return array Filtered array of doctors */ public function filter_doctors($doctors) { // Validate input if (!is_array($doctors)) { return $doctors; } // Skip filtering in admin area (keep full access for administrators) if (is_admin() && current_user_can('manage_options')) { return $doctors; } try { // Get blocked doctors (with caching) $blocked_doctors = $this->restriction_model->get_blocked_doctors(); if (empty($blocked_doctors)) { return $doctors; } // Filter out blocked doctors $filtered_doctors = []; foreach ($doctors as $key => $doctor) { // Handle both array and object formats $doctor_id = is_array($doctor) ? ($doctor['id'] ?? 0) : ($doctor->id ?? 0); if (!in_array((int) $doctor_id, $blocked_doctors)) { $filtered_doctors[$key] = $doctor; } } return $filtered_doctors; } catch (Exception $e) { // Log error and return original array on failure if (function_exists('error_log')) { error_log('Care Booking Block: Doctor filtering error - ' . $e->getMessage()); } return $doctors; } } /** * Filter KiviCare services list to remove blocked services for specific doctor * * @param array $services Array of services from KiviCare * @param int $doctor_id Doctor ID * @return array Filtered array of services */ public function filter_services($services, $doctor_id = null) { // Validate input if (!is_array($services)) { return $services; } // Skip filtering in admin area (keep full access for administrators) if (is_admin() && current_user_can('manage_options')) { return $services; } try { $filtered_services = []; // If no doctor_id provided, try to extract from services or context if (!$doctor_id) { $doctor_id = $this->extract_doctor_id_from_context($services); } // Get blocked services for this doctor (with enhanced caching) $blocked_services = $doctor_id ? $this->restriction_model->get_blocked_services($doctor_id) : []; // Also get globally blocked doctors to filter services $blocked_doctors = $this->restriction_model->get_blocked_doctors(); foreach ($services as $key => $service) { // Handle both array and object formats $service_id = is_array($service) ? ($service['id'] ?? 0) : ($service->id ?? 0); $service_doctor_id = is_array($service) ? ($service['doctor_id'] ?? $doctor_id) : ($service->doctor_id ?? $doctor_id); // Skip if service belongs to a blocked doctor if ($service_doctor_id && in_array((int) $service_doctor_id, $blocked_doctors)) { continue; } // Skip if service is specifically blocked for this doctor if ($service_doctor_id && !empty($blocked_services) && in_array((int) $service_id, $blocked_services)) { continue; } $filtered_services[$key] = $service; } return $filtered_services; } catch (Exception $e) { // Log error and return original array on failure if (function_exists('error_log')) { error_log('Care Booking Block: Service filtering error - ' . $e->getMessage()); } return $services; } } /** * Extract doctor ID from service context or URL parameters * * @param array $services Services array * @return int|null Doctor ID if found */ private function extract_doctor_id_from_context($services) { // Try to get from first service if (!empty($services)) { $first_service = reset($services); $doctor_id = is_array($first_service) ? ($first_service['doctor_id'] ?? null) : ($first_service->doctor_id ?? null); if ($doctor_id) { return (int) $doctor_id; } } // Try to get from URL parameters if (isset($_GET['doctor_id'])) { return (int) $_GET['doctor_id']; } // Try to get from POST data if (isset($_POST['doctor_id'])) { return (int) $_POST['doctor_id']; } return null; } /** * Enqueue frontend JavaScript for graceful degradation */ public function enqueue_frontend_scripts() { // Only on frontend and if KiviCare is active if (is_admin() || !$this->is_kivicare_active()) { return; } // Check if we're on a page that might have KiviCare content if (!$this->should_load_frontend_scripts()) { return; } wp_enqueue_script( 'care-booking-frontend', CARE_BOOKING_BLOCK_PLUGIN_URL . 'public/js/frontend.js', ['jquery'], CARE_BOOKING_BLOCK_VERSION, true ); // Localize script with configuration wp_localize_script('care-booking-frontend', 'careBookingConfig', [ 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('care_booking_frontend'), 'debug' => defined('WP_DEBUG') && WP_DEBUG, 'fallbackEnabled' => true, 'retryAttempts' => 3, 'retryDelay' => 1000, 'selectors' => [ 'doctors' => '.kivicare-doctor, .kc-doctor-item, .doctor-card', 'services' => '.kivicare-service, .kc-service-item, .service-card', 'forms' => '.kivicare-booking-form, .kc-booking-form', 'loading' => '.care-booking-loading' ] ]); } /** * Check if frontend scripts should be loaded on current page * * @return bool True if scripts should be loaded */ private function should_load_frontend_scripts() { global $post; // Always load on pages with KiviCare shortcodes if ($post && has_shortcode($post->post_content, 'kivicare')) { return true; } // Load on pages with KiviCare blocks if ($post && has_block('kivicare/booking', $post->post_content)) { return true; } // Load on template pages that might contain KiviCare $template = get_page_template_slug(); if (in_array($template, ['page-booking.php', 'page-appointment.php'])) { return true; } // Load if URL contains KiviCare parameters if (isset($_GET['kivicare']) || isset($_GET['booking']) || isset($_GET['appointment'])) { return true; } return false; } /** * Enqueue frontend CSS for base styles */ public function enqueue_frontend_styles() { // Only on frontend and if KiviCare is active if (is_admin() || !$this->is_kivicare_active()) { return; } // Check if we're on a page that might have KiviCare content if (!$this->should_load_frontend_scripts()) { return; } wp_enqueue_style( 'care-booking-frontend', CARE_BOOKING_BLOCK_PLUGIN_URL . 'public/css/frontend.css', [], CARE_BOOKING_BLOCK_VERSION, 'all' ); } /** * Inject optimized CSS to hide blocked elements on frontend * * Priority 15 - After theme styles but before most plugins */ public function inject_restriction_css() { // Only inject on frontend if (is_admin()) { return; } // Skip if not on pages with KiviCare content (performance optimization) if (!$this->should_inject_css()) { return; } try { // Get blocked doctors and services with caching $blocked_doctors = $this->restriction_model->get_blocked_doctors(); $blocked_services = $this->get_all_blocked_services(); // Early return if no restrictions if (empty($blocked_doctors) && empty($blocked_services)) { return; } // Generate optimized CSS $css = $this->generate_restriction_css($blocked_doctors, $blocked_services); if (!empty($css)) { // Output with proper caching headers and minification echo "\n\n"; echo ''; echo "\n\n"; } } catch (Exception $e) { // Silently fail to avoid breaking frontend if (function_exists('error_log')) { error_log('Care Booking Block: CSS injection error - ' . $e->getMessage()); } // In debug mode, show a minimal error indicator if (defined('WP_DEBUG') && WP_DEBUG) { echo ''; } } } /** * Determine if CSS should be injected on current page * * @return bool True if CSS should be injected */ private function should_inject_css() { // Always inject if KiviCare is active and we have restrictions if (!$this->is_kivicare_active()) { return false; } // Use the same logic as frontend scripts return $this->should_load_frontend_scripts(); } /** * Minify CSS for production * * @param string $css CSS to minify * @return string Minified CSS */ private function minify_css($css) { // Remove comments $css = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css); // Remove whitespace $css = str_replace(["\r\n", "\r", "\n", "\t"], '', $css); // Remove extra spaces $css = preg_replace('/\s+/', ' ', $css); // Remove spaces around specific characters $css = str_replace(['; ', ' {', '{ ', ' }', '} ', ': ', ', ', ' ,'], [';', '{', '{', '}', '}', ':', ',', ','], $css); return trim($css); } /** * Generate optimized CSS for hiding blocked elements * * @param array $blocked_doctors Array of blocked doctor IDs * @param array $blocked_services Array of blocked service data * @return string Generated CSS with optimization and caching */ private function generate_restriction_css($blocked_doctors, $blocked_services) { // Check cache first $cache_key = 'care_booking_css_' . md5(serialize([$blocked_doctors, $blocked_services])); $cached_css = get_transient($cache_key); if ($cached_css !== false) { return $cached_css; } $css_rules = []; $css_comments = []; // CSS for blocked doctors with enhanced selectors if (!empty($blocked_doctors)) { $doctor_selectors = []; $css_comments[] = "/* Blocked doctors: " . count($blocked_doctors) . " */"; foreach ($blocked_doctors as $doctor_id) { $doctor_id = (int) $doctor_id; // KiviCare 3.0+ primary selectors $doctor_selectors[] = ".kivicare-doctor[data-doctor-id=\"{$doctor_id}\"]"; $doctor_selectors[] = ".kc-doctor-item[data-id=\"{$doctor_id}\"]"; $doctor_selectors[] = ".doctor-card[data-doctor=\"{$doctor_id}\"]"; // Legacy selectors $doctor_selectors[] = "#doctor-{$doctor_id}"; $doctor_selectors[] = ".kc-doctor-{$doctor_id}"; // Form selectors $doctor_selectors[] = ".doctor-selection option[value=\"{$doctor_id}\"]"; $doctor_selectors[] = "select[name='doctor_id'] option[value=\"{$doctor_id}\"]"; // Booking form selectors $doctor_selectors[] = ".booking-doctor-{$doctor_id}"; $doctor_selectors[] = ".appointment-doctor-{$doctor_id}"; } if (!empty($doctor_selectors)) { // Split into chunks for better CSS performance $chunks = array_chunk($doctor_selectors, 50); foreach ($chunks as $chunk) { $css_rules[] = implode(',', $chunk) . ' { display: none !important; visibility: hidden !important; }'; } } } // CSS for blocked services with enhanced context if (!empty($blocked_services)) { $service_selectors = []; $css_comments[] = "/* Blocked services: " . count($blocked_services) . " */"; foreach ($blocked_services as $service_data) { $service_id = (int) $service_data['service_id']; $doctor_id = (int) $service_data['doctor_id']; // KiviCare 3.0+ primary selectors $service_selectors[] = ".kivicare-service[data-service-id=\"{$service_id}\"][data-doctor-id=\"{$doctor_id}\"]"; $service_selectors[] = ".kc-service-item[data-service=\"{$service_id}\"][data-doctor=\"{$doctor_id}\"]"; $service_selectors[] = ".service-card[data-service=\"{$service_id}\"][data-doctor=\"{$doctor_id}\"]"; // Legacy selectors $service_selectors[] = "#service-{$service_id}-doctor-{$doctor_id}"; $service_selectors[] = ".kc-service-{$service_id}.kc-doctor-{$doctor_id}"; // Form selectors $service_selectors[] = ".service-selection[data-doctor=\"{$doctor_id}\"] option[value=\"{$service_id}\"]"; $service_selectors[] = "select[name='service_id'][data-doctor=\"{$doctor_id}\"] option[value=\"{$service_id}\"]"; // Booking form selectors $service_selectors[] = ".booking-service-{$service_id}.doctor-{$doctor_id}"; $service_selectors[] = ".appointment-service-{$service_id}.doctor-{$doctor_id}"; } if (!empty($service_selectors)) { // Split into chunks for better CSS performance $chunks = array_chunk($service_selectors, 50); foreach ($chunks as $chunk) { $css_rules[] = implode(',', $chunk) . ' { display: none !important; visibility: hidden !important; }'; } } } // Add graceful degradation styles $css_rules[] = '.care-booking-fallback { opacity: 0.7; pointer-events: none; }'; $css_rules[] = '.care-booking-loading::after { content: "Loading..."; }'; // Combine CSS with optimization $final_css = ''; if (!empty($css_comments)) { $final_css .= implode(PHP_EOL, $css_comments) . PHP_EOL; } if (!empty($css_rules)) { // Minify CSS in production if (defined('WP_DEBUG') && !WP_DEBUG) { $final_css .= implode('', $css_rules); } else { $final_css .= implode(PHP_EOL, $css_rules); } } // Cache for 1 hour set_transient($cache_key, $final_css, 3600); return $final_css; } /** * Get all blocked services across all doctors * * @return array Array of blocked service data */ private function get_all_blocked_services() { $blocked_services = []; // Get all service restrictions $service_restrictions = $this->restriction_model->get_by_type('service'); foreach ($service_restrictions as $restriction) { if ($restriction->is_blocked) { $blocked_services[] = [ 'service_id' => (int) $restriction->target_id, 'doctor_id' => (int) $restriction->doctor_id ]; } } return $blocked_services; } /** * Add admin bar menu for quick access * * @param WP_Admin_Bar $wp_admin_bar WordPress admin bar object */ public function add_admin_bar_menu($wp_admin_bar) { // Only show for users with manage_options capability if (!current_user_can('manage_options')) { return; } $wp_admin_bar->add_menu([ 'id' => 'care-booking-control', 'title' => __('Care Booking', 'care-booking-block'), 'href' => admin_url('tools.php?page=care-booking-control'), 'meta' => [ 'title' => __('Care Booking Control', 'care-booking-block') ] ]); // Add submenu with statistics $stats = $this->restriction_model->get_statistics(); $wp_admin_bar->add_menu([ 'parent' => 'care-booking-control', 'id' => 'care-booking-stats', 'title' => sprintf( __('Restrictions: %d doctors, %d services', 'care-booking-block'), $stats['blocked_doctors'], $stats['service_restrictions'] ), 'href' => admin_url('tools.php?page=care-booking-control'), ]); } /** * Check if specific doctor is blocked * * @param int $doctor_id Doctor ID * @return bool True if blocked, false otherwise */ public function is_doctor_blocked($doctor_id) { return $this->restriction_model->is_doctor_blocked($doctor_id); } /** * Check if specific service is blocked for a doctor * * @param int $service_id Service ID * @param int $doctor_id Doctor ID * @return bool True if blocked, false otherwise */ public function is_service_blocked($service_id, $doctor_id) { return $this->restriction_model->is_service_blocked($service_id, $doctor_id); } /** * Get blocked doctors count * * @return int Number of blocked doctors */ public function get_blocked_doctors_count() { return count($this->restriction_model->get_blocked_doctors()); } /** * Get blocked services count for specific doctor * * @param int $doctor_id Doctor ID * @return int Number of blocked services */ public function get_blocked_services_count($doctor_id) { return count($this->restriction_model->get_blocked_services($doctor_id)); } /** * Apply restrictions to KiviCare query (if supported) * * @param string $query SQL query * @param string $context Query context * @return string Modified query */ public function filter_kivicare_query($query, $context = '') { // This would be used if KiviCare provides query filtering hooks // For now, return original query return $query; } /** * Handle KiviCare appointment booking validation * * @param array $booking_data Booking data * @return bool|WP_Error True if allowed, WP_Error if blocked */ public function validate_booking($booking_data) { $doctor_id = $booking_data['doctor_id'] ?? 0; $service_id = $booking_data['service_id'] ?? 0; // Check if doctor is blocked if ($this->is_doctor_blocked($doctor_id)) { return new WP_Error( 'doctor_blocked', __('This doctor is not available for booking.', 'care-booking-block') ); } // Check if service is blocked for this doctor if ($service_id && $this->is_service_blocked($service_id, $doctor_id)) { return new WP_Error( 'service_blocked', __('This service is not available for this doctor.', 'care-booking-block') ); } return true; } /** * Get integration status * * @return array Status information */ public function get_integration_status() { return [ 'kivicare_active' => $this->is_kivicare_active(), 'hooks_registered' => [ 'doctor_filter' => has_filter('kc_get_doctors_for_booking'), 'service_filter' => has_filter('kc_get_services_by_doctor'), 'css_injection' => has_action('wp_head') ], 'cache_status' => $this->cache_manager->get_cache_stats(), 'restrictions' => $this->restriction_model->get_statistics() ]; } /** * Filter KiviCare REST API responses for doctor and service listings * * @param mixed $served Whether the request has already been served * @param WP_HTTP_Response $result The response object * @param WP_REST_Request $request The request object * @param WP_REST_Server $server The REST server instance * @return mixed Original served value */ public function filter_rest_api_response($served, $result, $request, $server) { // Skip if already served or not a KiviCare endpoint if ($served || !$this->is_kivicare_rest_endpoint($request)) { return $served; } // Skip filtering in admin area for administrators if (is_admin() && current_user_can('manage_options')) { return $served; } try { $data = $result->get_data(); if (is_array($data) && isset($data['data'])) { $route = $request->get_route(); // Filter doctors endpoint if (strpos($route, '/doctors') !== false && is_array($data['data'])) { $data['data'] = $this->filter_doctors($data['data']); $result->set_data($data); } // Filter services endpoint if (strpos($route, '/services') !== false && is_array($data['data'])) { $doctor_id = $request->get_param('doctor_id') ?: null; $data['data'] = $this->filter_services($data['data'], $doctor_id); $result->set_data($data); } } } catch (Exception $e) { // Log error but don't break API response if (function_exists('error_log')) { error_log('Care Booking Block: REST API filtering error - ' . $e->getMessage()); } } return $served; } /** * Check if request is for a KiviCare REST endpoint * * @param WP_REST_Request $request The request object * @return bool True if KiviCare endpoint */ private function is_kivicare_rest_endpoint($request) { $route = $request->get_route(); return strpos($route, '/kivicare/') !== false || strpos($route, '/kc/') !== false; } /** * Check if KiviCare plugin is active * * @return bool True if KiviCare is active, false otherwise */ private function is_kivicare_active() { if (!function_exists('is_plugin_active')) { include_once(ABSPATH . 'wp-admin/includes/plugin.php'); } return is_plugin_active('kivicare/kivicare.php') || is_plugin_active('kivicare-clinic-management-system/kivicare.php'); } }