/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ * @link https://descomplicar.pt * @since 1.0.0 */ namespace Care_API\Models; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class Appointment * * Model for handling appointment scheduling, availability and management * * @since 1.0.0 */ class Appointment { /** * Database table name * * @var string */ private static $table_name = 'kc_appointments'; /** * Appointment ID * * @var int */ public $id; /** * Appointment data * * @var array */ private $data = array(); /** * Required fields for appointment creation * * @var array */ private static $required_fields = array( 'appointment_start_date', 'appointment_start_time', 'appointment_end_time', 'clinic_id', 'doctor_id', 'patient_id' ); /** * Valid appointment statuses * * @var array */ private static $valid_statuses = array( 1 => 'scheduled', 2 => 'completed', 3 => 'cancelled', 4 => 'no_show' ); /** * Constructor * * @param int|array $appointment_id_or_data Appointment ID or data array * @since 1.0.0 */ public function __construct( $appointment_id_or_data = null ) { if ( is_numeric( $appointment_id_or_data ) ) { $this->id = (int) $appointment_id_or_data; $this->load_data(); } elseif ( is_array( $appointment_id_or_data ) ) { $this->data = $appointment_id_or_data; $this->id = isset( $this->data['id'] ) ? (int) $this->data['id'] : null; } } /** * Load appointment data from database * * @return bool True on success, false on failure * @since 1.0.0 */ private function load_data() { if ( ! $this->id ) { return false; } $appointment_data = self::get_appointment_full_data( $this->id ); if ( $appointment_data ) { $this->data = $appointment_data; return true; } return false; } /** * Create a new appointment * * @param array $appointment_data Appointment data * @return int|WP_Error Appointment ID on success, WP_Error on failure * @since 1.0.0 */ public static function create( $appointment_data ) { // Validate required fields $validation = self::validate_appointment_data( $appointment_data ); if ( is_wp_error( $validation ) ) { return $validation; } // Check availability $availability_check = self::check_availability( $appointment_data ); if ( is_wp_error( $availability_check ) ) { return $availability_check; } global $wpdb; $table = $wpdb->prefix . self::$table_name; // Calculate end date (assume same day if not provided) $end_date = isset( $appointment_data['appointment_end_date'] ) ? $appointment_data['appointment_end_date'] : $appointment_data['appointment_start_date']; // Prepare data for insertion $insert_data = array( 'appointment_start_date' => sanitize_text_field( $appointment_data['appointment_start_date'] ), 'appointment_start_time' => sanitize_text_field( $appointment_data['appointment_start_time'] ), 'appointment_end_date' => sanitize_text_field( $end_date ), 'appointment_end_time' => sanitize_text_field( $appointment_data['appointment_end_time'] ), 'visit_type' => isset( $appointment_data['visit_type'] ) ? sanitize_text_field( $appointment_data['visit_type'] ) : 'consultation', 'clinic_id' => (int) $appointment_data['clinic_id'], 'doctor_id' => (int) $appointment_data['doctor_id'], 'patient_id' => (int) $appointment_data['patient_id'], 'description' => isset( $appointment_data['description'] ) ? sanitize_textarea_field( $appointment_data['description'] ) : '', 'status' => isset( $appointment_data['status'] ) ? (int) $appointment_data['status'] : 1, 'created_at' => current_time( 'mysql' ), 'appointment_report' => '' ); $result = $wpdb->insert( $table, $insert_data ); if ( $result === false ) { return new \WP_Error( 'appointment_creation_failed', 'Failed to create appointment: ' . $wpdb->last_error, array( 'status' => 500 ) ); } $appointment_id = $wpdb->insert_id; // Create service mappings if provided if ( ! empty( $appointment_data['services'] ) && is_array( $appointment_data['services'] ) ) { self::create_service_mappings( $appointment_id, $appointment_data['services'] ); } return $appointment_id; } /** * Update appointment data * * @param int $appointment_id Appointment ID * @param array $appointment_data Updated appointment data * @return bool|WP_Error True on success, WP_Error on failure * @since 1.0.0 */ public static function update( $appointment_id, $appointment_data ) { if ( ! self::exists( $appointment_id ) ) { return new \WP_Error( 'appointment_not_found', 'Appointment not found', array( 'status' => 404 ) ); } // If updating schedule details, check availability if ( isset( $appointment_data['appointment_start_date'] ) || isset( $appointment_data['appointment_start_time'] ) || isset( $appointment_data['appointment_end_time'] ) || isset( $appointment_data['doctor_id'] ) ) { $current_data = self::get_by_id( $appointment_id ); $check_data = array_merge( $current_data, $appointment_data ); $check_data['exclude_appointment_id'] = $appointment_id; $availability_check = self::check_availability( $check_data ); if ( is_wp_error( $availability_check ) ) { return $availability_check; } } global $wpdb; $table = $wpdb->prefix . self::$table_name; // Prepare update data $update_data = array(); $allowed_fields = array( 'appointment_start_date', 'appointment_start_time', 'appointment_end_date', 'appointment_end_time', 'visit_type', 'clinic_id', 'doctor_id', 'patient_id', 'description', 'status', 'appointment_report' ); foreach ( $allowed_fields as $field ) { if ( isset( $appointment_data[ $field ] ) ) { $value = $appointment_data[ $field ]; switch ( $field ) { case 'clinic_id': case 'doctor_id': case 'patient_id': case 'status': $update_data[ $field ] = (int) $value; break; case 'description': case 'appointment_report': $update_data[ $field ] = sanitize_textarea_field( $value ); break; default: $update_data[ $field ] = sanitize_text_field( $value ); } } } if ( empty( $update_data ) ) { return new \WP_Error( 'no_data_to_update', 'No valid data provided for update', array( 'status' => 400 ) ); } $result = $wpdb->update( $table, $update_data, array( 'id' => $appointment_id ), null, array( '%d' ) ); if ( $result === false ) { return new \WP_Error( 'appointment_update_failed', 'Failed to update appointment: ' . $wpdb->last_error, array( 'status' => 500 ) ); } // Update service mappings if provided if ( isset( $appointment_data['services'] ) && is_array( $appointment_data['services'] ) ) { self::update_service_mappings( $appointment_id, $appointment_data['services'] ); } return true; } /** * Delete an appointment * * @param int $appointment_id Appointment ID * @return bool|WP_Error True on success, WP_Error on failure * @since 1.0.0 */ public static function delete( $appointment_id ) { if ( ! self::exists( $appointment_id ) ) { return new \WP_Error( 'appointment_not_found', 'Appointment not found', array( 'status' => 404 ) ); } // Check if appointment can be deleted (not completed with encounters) if ( self::has_completed_encounter( $appointment_id ) ) { return new \WP_Error( 'appointment_has_encounter', 'Cannot delete appointment with completed encounter', array( 'status' => 409 ) ); } global $wpdb; // Delete service mappings first $wpdb->delete( "{$wpdb->prefix}kc_appointment_service_mapping", array( 'appointment_id' => $appointment_id ), array( '%d' ) ); // Delete appointment $table = $wpdb->prefix . self::$table_name; $result = $wpdb->delete( $table, array( 'id' => $appointment_id ), array( '%d' ) ); if ( $result === false ) { return new \WP_Error( 'appointment_deletion_failed', 'Failed to delete appointment: ' . $wpdb->last_error, array( 'status' => 500 ) ); } return true; } /** * Get appointment by ID * * @param int $appointment_id Appointment ID * @return array|null Appointment data or null if not found * @since 1.0.0 */ public static function get_by_id( $appointment_id ) { return self::get_appointment_full_data( $appointment_id ); } /** * Get all appointments with optional filtering * * @param array $args Query arguments * @return array Array of appointment data * @since 1.0.0 */ public static function get_all( $args = array() ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $defaults = array( 'clinic_id' => null, 'doctor_id' => null, 'patient_id' => null, 'status' => null, 'date_from' => null, 'date_to' => null, 'search' => '', 'limit' => 50, 'offset' => 0, 'orderby' => 'appointment_start_date', 'order' => 'ASC' ); $args = wp_parse_args( $args, $defaults ); $where_clauses = array( '1=1' ); $where_values = array(); // Clinic filter if ( ! is_null( $args['clinic_id'] ) ) { $where_clauses[] = 'a.clinic_id = %d'; $where_values[] = $args['clinic_id']; } // Doctor filter if ( ! is_null( $args['doctor_id'] ) ) { $where_clauses[] = 'a.doctor_id = %d'; $where_values[] = $args['doctor_id']; } // Patient filter if ( ! is_null( $args['patient_id'] ) ) { $where_clauses[] = 'a.patient_id = %d'; $where_values[] = $args['patient_id']; } // Status filter if ( ! is_null( $args['status'] ) ) { $where_clauses[] = 'a.status = %d'; $where_values[] = $args['status']; } // Date range filters if ( ! is_null( $args['date_from'] ) ) { $where_clauses[] = 'a.appointment_start_date >= %s'; $where_values[] = $args['date_from']; } if ( ! is_null( $args['date_to'] ) ) { $where_clauses[] = 'a.appointment_start_date <= %s'; $where_values[] = $args['date_to']; } // Search filter if ( ! empty( $args['search'] ) ) { $where_clauses[] = '(p.first_name LIKE %s OR p.last_name LIKE %s OR d.first_name LIKE %s OR d.last_name LIKE %s OR c.name LIKE %s)'; $search_term = '%' . $wpdb->esc_like( $args['search'] ) . '%'; $where_values = array_merge( $where_values, array_fill( 0, 5, $search_term ) ); } $where_sql = implode( ' AND ', $where_clauses ); // Build query $query = "SELECT a.*, c.name as clinic_name, CONCAT(p.first_name, ' ', p.last_name) as patient_name, p.user_email as patient_email, CONCAT(d.first_name, ' ', d.last_name) as doctor_name, d.user_email as doctor_email FROM {$table} a LEFT JOIN {$wpdb->prefix}kc_clinics c ON a.clinic_id = c.id LEFT JOIN {$wpdb->prefix}users p ON a.patient_id = p.ID LEFT JOIN {$wpdb->prefix}users d ON a.doctor_id = d.ID WHERE {$where_sql}"; $query .= sprintf( ' ORDER BY a.%s %s', sanitize_sql_orderby( $args['orderby'] ), sanitize_sql_orderby( $args['order'] ) ); $query .= $wpdb->prepare( ' LIMIT %d OFFSET %d', $args['limit'], $args['offset'] ); if ( ! empty( $where_values ) ) { $query = $wpdb->prepare( $query, $where_values ); } $appointments = $wpdb->get_results( $query, ARRAY_A ); return array_map( array( self::class, 'format_appointment_data' ), $appointments ); } /** * Get available time slots for a doctor on a specific date * * @param array $args Availability parameters * @return array|WP_Error Available slots or error * @since 1.0.0 */ public static function get_available_slots( $args ) { $defaults = array( 'doctor_id' => null, 'clinic_id' => null, 'date' => date( 'Y-m-d' ), 'duration' => 30 // minutes ); $args = wp_parse_args( $args, $defaults ); if ( is_null( $args['doctor_id'] ) ) { return new \WP_Error( 'missing_doctor_id', 'Doctor ID is required', array( 'status' => 400 ) ); } // Get doctor's working hours for the day $day_of_week = strtolower( date( 'l', strtotime( $args['date'] ) ) ); $working_hours = get_user_meta( $args['doctor_id'], 'working_hours', true ); if ( is_string( $working_hours ) ) { $working_hours = json_decode( $working_hours, true ) ?: array(); } if ( empty( $working_hours[ $day_of_week ] ) || ! isset( $working_hours[ $day_of_week ]['start_time'] ) || ! isset( $working_hours[ $day_of_week ]['end_time'] ) ) { return array( 'available_slots' => array(), 'message' => 'Doctor is not available on this day' ); } $start_time = $working_hours[ $day_of_week ]['start_time']; $end_time = $working_hours[ $day_of_week ]['end_time']; // Get existing appointments for the day global $wpdb; $table = $wpdb->prefix . self::$table_name; $where_clauses = array( 'doctor_id = %d', 'appointment_start_date = %s', 'status IN (1, 2)' // scheduled or completed ); $where_values = array( $args['doctor_id'], $args['date'] ); if ( ! is_null( $args['clinic_id'] ) ) { $where_clauses[] = 'clinic_id = %d'; $where_values[] = $args['clinic_id']; } $existing_appointments = $wpdb->get_results( $wpdb->prepare( "SELECT appointment_start_time, appointment_end_time FROM {$table} WHERE " . implode( ' AND ', $where_clauses ), $where_values ), ARRAY_A ); // Generate all possible slots $all_slots = self::generate_time_slots( $start_time, $end_time, $args['duration'] ); // Remove booked slots $available_slots = array(); foreach ( $all_slots as $slot ) { $is_available = true; foreach ( $existing_appointments as $appointment ) { if ( self::times_overlap( $slot['start_time'], $slot['end_time'], $appointment['appointment_start_time'], $appointment['appointment_end_time'] ) ) { $is_available = false; break; } } if ( $is_available ) { $available_slots[] = $slot; } } return array( 'date' => $args['date'], 'doctor_id' => $args['doctor_id'], 'clinic_id' => $args['clinic_id'], 'working_hours' => array( 'start' => $start_time, 'end' => $end_time ), 'slot_duration' => $args['duration'], 'total_slots' => count( $all_slots ), 'available_slots' => $available_slots, 'booked_slots' => count( $all_slots ) - count( $available_slots ) ); } /** * Check appointment availability * * @param array $appointment_data Appointment data to check * @return bool|WP_Error True if available, WP_Error if conflict * @since 1.0.0 */ public static function check_availability( $appointment_data ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $where_clauses = array( 'doctor_id = %d', 'appointment_start_date = %s', 'status IN (1, 2)' // scheduled or completed ); $where_values = array( $appointment_data['doctor_id'], $appointment_data['appointment_start_date'] ); // Exclude current appointment if updating if ( isset( $appointment_data['exclude_appointment_id'] ) ) { $where_clauses[] = 'id != %d'; $where_values[] = $appointment_data['exclude_appointment_id']; } $conflicting_appointments = $wpdb->get_results( $wpdb->prepare( "SELECT id, appointment_start_time, appointment_end_time FROM {$table} WHERE " . implode( ' AND ', $where_clauses ), $where_values ), ARRAY_A ); $start_time = $appointment_data['appointment_start_time']; $end_time = $appointment_data['appointment_end_time']; foreach ( $conflicting_appointments as $existing ) { if ( self::times_overlap( $start_time, $end_time, $existing['appointment_start_time'], $existing['appointment_end_time'] ) ) { return new \WP_Error( 'appointment_conflict', 'Doctor is not available at the requested time', array( 'status' => 409, 'conflicting_appointment_id' => $existing['id'] ) ); } } return true; } /** * Get appointment full data with related entities * * @param int $appointment_id Appointment ID * @return array|null Full appointment data or null if not found * @since 1.0.0 */ public static function get_appointment_full_data( $appointment_id ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $appointment = $wpdb->get_row( $wpdb->prepare( "SELECT a.*, c.name as clinic_name, c.address as clinic_address, CONCAT(p.first_name, ' ', p.last_name) as patient_name, p.user_email as patient_email, CONCAT(d.first_name, ' ', d.last_name) as doctor_name, d.user_email as doctor_email FROM {$table} a LEFT JOIN {$wpdb->prefix}kc_clinics c ON a.clinic_id = c.id LEFT JOIN {$wpdb->prefix}users p ON a.patient_id = p.ID LEFT JOIN {$wpdb->prefix}users d ON a.doctor_id = d.ID WHERE a.id = %d", $appointment_id ), ARRAY_A ); if ( ! $appointment ) { return null; } return self::format_appointment_data( $appointment ); } /** * Get appointment services * * @param int $appointment_id Appointment ID * @return array Array of services * @since 1.0.0 */ public static function get_services( $appointment_id ) { global $wpdb; $services = $wpdb->get_results( $wpdb->prepare( "SELECT s.*, asm.created_at as mapped_at FROM {$wpdb->prefix}kc_appointment_service_mapping asm LEFT JOIN {$wpdb->prefix}kc_services s ON asm.service_id = s.id WHERE asm.appointment_id = %d", $appointment_id ), ARRAY_A ); return array_map( function( $service ) { return array( 'id' => (int) $service['id'], 'name' => $service['name'], 'type' => $service['type'], 'price' => (float) $service['price'], 'status' => (int) $service['status'], 'mapped_at' => $service['mapped_at'] ); }, $services ); } /** * Check if appointment exists * * @param int $appointment_id Appointment ID * @return bool True if exists, false otherwise * @since 1.0.0 */ public static function exists( $appointment_id ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE id = %d", $appointment_id ) ); return (int) $count > 0; } /** * Check if appointment has completed encounter * * @param int $appointment_id Appointment ID * @return bool True if has encounter, false otherwise * @since 1.0.0 */ private static function has_completed_encounter( $appointment_id ) { global $wpdb; $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}kc_patient_encounters WHERE appointment_id = %d AND status = 1", $appointment_id ) ); return (int) $count > 0; } /** * Validate appointment data * * @param array $appointment_data Appointment data to validate * @return bool|WP_Error True if valid, WP_Error if invalid * @since 1.0.0 */ private static function validate_appointment_data( $appointment_data ) { $errors = array(); // Check required fields foreach ( self::$required_fields as $field ) { if ( empty( $appointment_data[ $field ] ) ) { $errors[] = "Field '{$field}' is required"; } } // Validate date format if ( ! empty( $appointment_data['appointment_start_date'] ) ) { $date = \DateTime::createFromFormat( 'Y-m-d', $appointment_data['appointment_start_date'] ); if ( ! $date || $date->format( 'Y-m-d' ) !== $appointment_data['appointment_start_date'] ) { $errors[] = 'Invalid start date format. Use YYYY-MM-DD'; } } // Validate time formats $time_fields = array( 'appointment_start_time', 'appointment_end_time' ); foreach ( $time_fields as $field ) { if ( ! empty( $appointment_data[ $field ] ) ) { if ( ! preg_match( '/^([01]?[0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?$/', $appointment_data[ $field ] ) ) { $errors[] = "Invalid {$field} format. Use HH:MM or HH:MM:SS"; } } } // Check if end time is after start time if ( ! empty( $appointment_data['appointment_start_time'] ) && ! empty( $appointment_data['appointment_end_time'] ) ) { $start = strtotime( $appointment_data['appointment_start_time'] ); $end = strtotime( $appointment_data['appointment_end_time'] ); if ( $end <= $start ) { $errors[] = 'End time must be after start time'; } } // Validate entities exist if ( ! empty( $appointment_data['clinic_id'] ) && ! Clinic::exists( $appointment_data['clinic_id'] ) ) { $errors[] = 'Clinic not found'; } if ( ! empty( $appointment_data['doctor_id'] ) && ! Doctor::exists( $appointment_data['doctor_id'] ) ) { $errors[] = 'Doctor not found'; } if ( ! empty( $appointment_data['patient_id'] ) && ! Patient::exists( $appointment_data['patient_id'] ) ) { $errors[] = 'Patient not found'; } // Validate status if ( isset( $appointment_data['status'] ) && ! array_key_exists( $appointment_data['status'], self::$valid_statuses ) ) { $errors[] = 'Invalid status value'; } if ( ! empty( $errors ) ) { return new \WP_Error( 'appointment_validation_failed', 'Appointment validation failed', array( 'status' => 400, 'errors' => $errors ) ); } return true; } /** * Format appointment data for API response * * @param array $appointment_data Raw appointment data * @return array Formatted appointment data * @since 1.0.0 */ private static function format_appointment_data( $appointment_data ) { if ( ! $appointment_data ) { return null; } $formatted = array( 'id' => (int) $appointment_data['id'], 'start_date' => $appointment_data['appointment_start_date'], 'start_time' => $appointment_data['appointment_start_time'], 'end_date' => $appointment_data['appointment_end_date'], 'end_time' => $appointment_data['appointment_end_time'], 'visit_type' => $appointment_data['visit_type'], 'status' => (int) $appointment_data['status'], 'status_text' => self::$valid_statuses[ $appointment_data['status'] ] ?? 'unknown', 'description' => $appointment_data['description'], 'appointment_report' => $appointment_data['appointment_report'], 'created_at' => $appointment_data['created_at'], 'clinic' => array( 'id' => (int) $appointment_data['clinic_id'], 'name' => $appointment_data['clinic_name'] ?? '', 'address' => $appointment_data['clinic_address'] ?? '' ), 'patient' => array( 'id' => (int) $appointment_data['patient_id'], 'name' => $appointment_data['patient_name'] ?? '', 'email' => $appointment_data['patient_email'] ?? '' ), 'doctor' => array( 'id' => (int) $appointment_data['doctor_id'], 'name' => $appointment_data['doctor_name'] ?? '', 'email' => $appointment_data['doctor_email'] ?? '' ) ); return $formatted; } /** * Create service mappings for appointment * * @param int $appointment_id Appointment ID * @param array $service_ids Array of service IDs * @return bool True on success * @since 1.0.0 */ private static function create_service_mappings( $appointment_id, $service_ids ) { global $wpdb; foreach ( $service_ids as $service_id ) { $wpdb->insert( "{$wpdb->prefix}kc_appointment_service_mapping", array( 'appointment_id' => $appointment_id, 'service_id' => (int) $service_id, 'created_at' => current_time( 'mysql' ) ), array( '%d', '%d', '%s' ) ); } return true; } /** * Update service mappings for appointment * * @param int $appointment_id Appointment ID * @param array $service_ids Array of service IDs * @return bool True on success * @since 1.0.0 */ private static function update_service_mappings( $appointment_id, $service_ids ) { global $wpdb; // Delete existing mappings $wpdb->delete( "{$wpdb->prefix}kc_appointment_service_mapping", array( 'appointment_id' => $appointment_id ), array( '%d' ) ); // Create new mappings return self::create_service_mappings( $appointment_id, $service_ids ); } /** * Generate time slots between start and end time * * @param string $start_time Start time (HH:MM) * @param string $end_time End time (HH:MM) * @param int $duration Slot duration in minutes * @return array Array of time slots * @since 1.0.0 */ private static function generate_time_slots( $start_time, $end_time, $duration ) { $slots = array(); $current = strtotime( $start_time ); $end = strtotime( $end_time ); while ( $current < $end ) { $slot_end = $current + ( $duration * 60 ); if ( $slot_end <= $end ) { $slots[] = array( 'start_time' => date( 'H:i', $current ), 'end_time' => date( 'H:i', $slot_end ), 'duration' => $duration ); } $current = $slot_end; } return $slots; } /** * Check if two time ranges overlap * * @param string $start1 First range start time * @param string $end1 First range end time * @param string $start2 Second range start time * @param string $end2 Second range end time * @return bool True if overlapping * @since 1.0.0 */ private static function times_overlap( $start1, $end1, $start2, $end2 ) { $start1_ts = strtotime( $start1 ); $end1_ts = strtotime( $end1 ); $start2_ts = strtotime( $start2 ); $end2_ts = strtotime( $end2 ); return ( $start1_ts < $end2_ts && $end1_ts > $start2_ts ); } /** * Get appointment statistics * * @param array $filters Optional filters * @return array Appointment statistics * @since 1.0.0 */ public static function get_statistics( $filters = array() ) { global $wpdb; $table = $wpdb->prefix . self::$table_name; $where_clauses = array( '1=1' ); $where_values = array(); if ( ! empty( $filters['clinic_id'] ) ) { $where_clauses[] = 'clinic_id = %d'; $where_values[] = $filters['clinic_id']; } if ( ! empty( $filters['doctor_id'] ) ) { $where_clauses[] = 'doctor_id = %d'; $where_values[] = $filters['doctor_id']; } $where_sql = implode( ' AND ', $where_clauses ); $stats = array( 'total_appointments' => 0, 'scheduled_appointments' => 0, 'completed_appointments' => 0, 'cancelled_appointments' => 0, 'no_show_appointments' => 0, 'appointments_today' => 0, 'appointments_this_week' => 0, 'appointments_this_month' => 0 ); // Total appointments $query = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}"; if ( ! empty( $where_values ) ) { $query = $wpdb->prepare( $query, $where_values ); } $stats['total_appointments'] = (int) $wpdb->get_var( $query ); // Appointments by status foreach ( self::$valid_statuses as $status_id => $status_name ) { $status_where = $where_clauses; $status_where[] = 'status = %d'; $status_values = array_merge( $where_values, array( $status_id ) ); $query = $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $status_where ), $status_values ); $stats[ $status_name . '_appointments' ] = (int) $wpdb->get_var( $query ); } // Appointments today $today_where = array_merge( $where_clauses, array( 'appointment_start_date = CURDATE()' ) ); $query = "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $today_where ); if ( ! empty( $where_values ) ) { $query = $wpdb->prepare( $query, $where_values ); } $stats['appointments_today'] = (int) $wpdb->get_var( $query ); // Appointments this week $week_where = array_merge( $where_clauses, array( 'WEEK(appointment_start_date) = WEEK(CURDATE())', 'YEAR(appointment_start_date) = YEAR(CURDATE())' ) ); $query = "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $week_where ); if ( ! empty( $where_values ) ) { $query = $wpdb->prepare( $query, $where_values ); } $stats['appointments_this_week'] = (int) $wpdb->get_var( $query ); // Appointments this month $month_where = array_merge( $where_clauses, array( 'MONTH(appointment_start_date) = MONTH(CURDATE())', 'YEAR(appointment_start_date) = YEAR(CURDATE())' ) ); $query = "SELECT COUNT(*) FROM {$table} WHERE " . implode( ' AND ', $month_where ); if ( ! empty( $where_values ) ) { $query = $wpdb->prepare( $query, $where_values ); } $stats['appointments_this_month'] = (int) $wpdb->get_var( $query ); return $stats; } }