* @link https://descomplicar.pt * @since 1.0.0 */ namespace Care_API\Services\Database; use Care_API\Models\Appointment; use Care_API\Services\Permission_Service; if ( ! defined( 'ABSPATH' ) ) { exit; } /** * Class Appointment_Service * * Advanced database service for appointment management with business logic * * @since 1.0.0 */ class Appointment_Service { /** * Appointment status constants */ const STATUS_BOOKED = 1; const STATUS_COMPLETED = 2; const STATUS_CANCELLED = 3; const STATUS_NO_SHOW = 4; const STATUS_RESCHEDULED = 5; /** * Initialize the service * * @since 1.0.0 */ public static function init() { // Hook into WordPress actions add_action( 'kivicare_appointment_created', array( self::class, 'on_appointment_created' ), 10, 2 ); add_action( 'kivicare_appointment_updated', array( self::class, 'on_appointment_updated' ), 10, 2 ); add_action( 'kivicare_appointment_cancelled', array( self::class, 'on_appointment_cancelled' ), 10, 1 ); add_action( 'kivicare_appointment_completed', array( self::class, 'on_appointment_completed' ), 10, 1 ); } /** * Create appointment with advanced business logic * * @param array $appointment_data Appointment data * @param int $user_id Creating user ID * @return array|WP_Error Appointment data or error * @since 1.0.0 */ public static function create_appointment( $appointment_data, $user_id = null ) { // Permission check if ( ! Permission_Service::can_manage_appointments( get_current_user_id(), $appointment_data['clinic_id'] ?? 0 ) ) { return new \WP_Error( 'insufficient_permissions', 'You do not have permission to create appointments', array( 'status' => 403 ) ); } // Enhanced validation $validation = self::validate_appointment_business_rules( $appointment_data ); if ( is_wp_error( $validation ) ) { return $validation; } // Check doctor availability $availability_check = self::check_doctor_availability( $appointment_data ); if ( is_wp_error( $availability_check ) ) { return $availability_check; } // Add metadata $appointment_data['created_by'] = $user_id ?: get_current_user_id(); $appointment_data['created_at'] = current_time( 'mysql' ); $appointment_data['status'] = self::STATUS_BOOKED; // Generate appointment number if not provided if ( empty( $appointment_data['appointment_number'] ) ) { $appointment_data['appointment_number'] = self::generate_appointment_number(); } // Calculate end time if not provided if ( empty( $appointment_data['appointment_end_time'] ) ) { $appointment_data['appointment_end_time'] = self::calculate_end_time( $appointment_data['appointment_start_time'], $appointment_data['duration'] ?? 30 ); } // Create appointment $appointment_id = Appointment::create( $appointment_data ); if ( is_wp_error( $appointment_id ) ) { return $appointment_id; } // Post-creation tasks self::setup_appointment_defaults( $appointment_id, $appointment_data ); // Send notifications self::send_appointment_notifications( $appointment_id, 'created' ); // Trigger action do_action( 'kivicare_appointment_created', $appointment_id, $appointment_data ); // Return full appointment data return self::get_appointment_with_metadata( $appointment_id ); } /** * Update appointment with business logic * * @param int $appointment_id Appointment ID * @param array $appointment_data Updated data * @return array|WP_Error Updated appointment data or error * @since 1.0.0 */ public static function update_appointment( $appointment_id, $appointment_data ) { // Get current appointment data $current_appointment = Appointment::get_by_id( $appointment_id ); if ( ! $current_appointment ) { return new \WP_Error( 'appointment_not_found', 'Appointment not found', array( 'status' => 404 ) ); } // Permission check if ( ! Permission_Service::can_manage_appointments( get_current_user_id(), $current_appointment['clinic_id'] ) ) { return new \WP_Error( 'insufficient_permissions', 'You do not have permission to update this appointment', array( 'status' => 403 ) ); } // Enhanced validation $validation = self::validate_appointment_business_rules( $appointment_data, $appointment_id ); if ( is_wp_error( $validation ) ) { return $validation; } // Check if this is a rescheduling $is_rescheduling = self::is_appointment_rescheduling( $current_appointment, $appointment_data ); if ( $is_rescheduling ) { $availability_check = self::check_doctor_availability( $appointment_data, $appointment_id ); if ( is_wp_error( $availability_check ) ) { return $availability_check; } } // Add update metadata $appointment_data['updated_by'] = get_current_user_id(); $appointment_data['updated_at'] = current_time( 'mysql' ); // Update appointment $result = Appointment::update( $appointment_id, $appointment_data ); if ( is_wp_error( $result ) ) { return $result; } // Handle status changes self::handle_status_changes( $appointment_id, $current_appointment, $appointment_data ); // Send notifications if needed if ( $is_rescheduling ) { self::send_appointment_notifications( $appointment_id, 'rescheduled' ); } // Trigger action do_action( 'kivicare_appointment_updated', $appointment_id, $appointment_data ); // Return updated appointment data return self::get_appointment_with_metadata( $appointment_id ); } /** * Cancel appointment * * @param int $appointment_id Appointment ID * @param string $reason Cancellation reason * @return array|WP_Error Updated appointment data or error * @since 1.0.0 */ public static function cancel_appointment( $appointment_id, $reason = '' ) { $appointment = Appointment::get_by_id( $appointment_id ); if ( ! $appointment ) { return new \WP_Error( 'appointment_not_found', 'Appointment not found', array( 'status' => 404 ) ); } // Permission check if ( ! Permission_Service::can_manage_appointments( get_current_user_id(), $appointment['clinic_id'] ) ) { return new \WP_Error( 'insufficient_permissions', 'You do not have permission to cancel this appointment', array( 'status' => 403 ) ); } // Check if appointment can be cancelled if ( $appointment['status'] == self::STATUS_COMPLETED ) { return new \WP_Error( 'cannot_cancel_completed', 'Cannot cancel a completed appointment', array( 'status' => 400 ) ); } if ( $appointment['status'] == self::STATUS_CANCELLED ) { return new \WP_Error( 'already_cancelled', 'Appointment is already cancelled', array( 'status' => 400 ) ); } // Update appointment status $update_data = array( 'status' => self::STATUS_CANCELLED, 'cancellation_reason' => $reason, 'cancelled_by' => get_current_user_id(), 'cancelled_at' => current_time( 'mysql' ), 'updated_at' => current_time( 'mysql' ) ); $result = Appointment::update( $appointment_id, $update_data ); if ( is_wp_error( $result ) ) { return $result; } // Send cancellation notifications self::send_appointment_notifications( $appointment_id, 'cancelled' ); // Trigger action do_action( 'kivicare_appointment_cancelled', $appointment_id ); return self::get_appointment_with_metadata( $appointment_id ); } /** * Complete appointment * * @param int $appointment_id Appointment ID * @param array $completion_data Completion data * @return array|WP_Error Updated appointment data or error * @since 1.0.0 */ public static function complete_appointment( $appointment_id, $completion_data = array() ) { $appointment = Appointment::get_by_id( $appointment_id ); if ( ! $appointment ) { return new \WP_Error( 'appointment_not_found', 'Appointment not found', array( 'status' => 404 ) ); } // Permission check if ( ! Permission_Service::can_manage_appointments( get_current_user_id(), $appointment['clinic_id'] ) ) { return new \WP_Error( 'insufficient_permissions', 'You do not have permission to complete this appointment', array( 'status' => 403 ) ); } // Check if appointment can be completed if ( $appointment['status'] == self::STATUS_CANCELLED ) { return new \WP_Error( 'cannot_complete_cancelled', 'Cannot complete a cancelled appointment', array( 'status' => 400 ) ); } if ( $appointment['status'] == self::STATUS_COMPLETED ) { return new \WP_Error( 'already_completed', 'Appointment is already completed', array( 'status' => 400 ) ); } // Update appointment status $update_data = array_merge( $completion_data, array( 'status' => self::STATUS_COMPLETED, 'completed_by' => get_current_user_id(), 'completed_at' => current_time( 'mysql' ), 'updated_at' => current_time( 'mysql' ) )); $result = Appointment::update( $appointment_id, $update_data ); if ( is_wp_error( $result ) ) { return $result; } // Auto-generate bill if configured if ( get_option( 'kivicare_auto_generate_bills', true ) ) { self::auto_generate_bill( $appointment_id ); } // Send completion notifications self::send_appointment_notifications( $appointment_id, 'completed' ); // Trigger action do_action( 'kivicare_appointment_completed', $appointment_id ); return self::get_appointment_with_metadata( $appointment_id ); } /** * Get appointment with enhanced metadata * * @param int $appointment_id Appointment ID * @return array|WP_Error Appointment data with metadata or error * @since 1.0.0 */ public static function get_appointment_with_metadata( $appointment_id ) { $appointment = Appointment::get_by_id( $appointment_id ); if ( ! $appointment ) { return new \WP_Error( 'appointment_not_found', 'Appointment not found', array( 'status' => 404 ) ); } // Permission check if ( ! Permission_Service::can_view_appointment( get_current_user_id(), $appointment_id ) ) { return new \WP_Error( 'access_denied', 'You do not have access to this appointment', array( 'status' => 403 ) ); } // Add enhanced metadata $appointment['patient'] = self::get_appointment_patient( $appointment['patient_id'] ); $appointment['doctor'] = self::get_appointment_doctor( $appointment['doctor_id'] ); $appointment['clinic'] = self::get_appointment_clinic( $appointment['clinic_id'] ); $appointment['service'] = self::get_appointment_service( $appointment['service_id'] ?? null ); $appointment['encounters'] = self::get_appointment_encounters( $appointment_id ); $appointment['bills'] = self::get_appointment_bills( $appointment_id ); $appointment['status_label'] = self::get_status_label( $appointment['status'] ); return $appointment; } /** * Search appointments with advanced criteria * * @param array $filters Search filters * @return array Search results * @since 1.0.0 */ public static function search_appointments( $filters = array() ) { global $wpdb; $user_id = get_current_user_id(); $accessible_clinic_ids = Permission_Service::get_accessible_clinic_ids( $user_id ); if ( empty( $accessible_clinic_ids ) ) { return array(); } // Build search query $where_clauses = array( "a.clinic_id IN (" . implode( ',', $accessible_clinic_ids ) . ")" ); $where_values = array(); // Date range filter if ( ! empty( $filters['start_date'] ) ) { $where_clauses[] = "DATE(a.appointment_start_date) >= %s"; $where_values[] = $filters['start_date']; } if ( ! empty( $filters['end_date'] ) ) { $where_clauses[] = "DATE(a.appointment_start_date) <= %s"; $where_values[] = $filters['end_date']; } // Doctor filter if ( ! empty( $filters['doctor_id'] ) ) { $where_clauses[] = "a.doctor_id = %d"; $where_values[] = $filters['doctor_id']; } // Patient filter if ( ! empty( $filters['patient_id'] ) ) { $where_clauses[] = "a.patient_id = %d"; $where_values[] = $filters['patient_id']; } // Clinic filter if ( ! empty( $filters['clinic_id'] ) && in_array( $filters['clinic_id'], $accessible_clinic_ids ) ) { $where_clauses[] = "a.clinic_id = %d"; $where_values[] = $filters['clinic_id']; } // Status filter if ( ! empty( $filters['status'] ) ) { if ( is_array( $filters['status'] ) ) { $status_placeholders = implode( ',', array_fill( 0, count( $filters['status'] ), '%d' ) ); $where_clauses[] = "a.status IN ({$status_placeholders})"; $where_values = array_merge( $where_values, $filters['status'] ); } else { $where_clauses[] = "a.status = %d"; $where_values[] = $filters['status']; } } // Search term if ( ! empty( $filters['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 a.appointment_number LIKE %s)"; $search_term = '%' . $wpdb->esc_like( $filters['search'] ) . '%'; $where_values = array_merge( $where_values, array_fill( 0, 5, $search_term ) ); } $where_sql = implode( ' AND ', $where_clauses ); // Pagination $limit = isset( $filters['limit'] ) ? (int) $filters['limit'] : 20; $offset = isset( $filters['offset'] ) ? (int) $filters['offset'] : 0; $query = "SELECT a.*, p.first_name as patient_first_name, p.last_name as patient_last_name, d.first_name as doctor_first_name, d.last_name as doctor_last_name, c.name as clinic_name FROM {$wpdb->prefix}kc_appointments a LEFT JOIN {$wpdb->prefix}users p ON a.patient_id = p.ID LEFT JOIN {$wpdb->prefix}users d ON a.doctor_id = d.ID LEFT JOIN {$wpdb->prefix}kc_clinics c ON a.clinic_id = c.id WHERE {$where_sql} ORDER BY a.appointment_start_date DESC, a.appointment_start_time DESC LIMIT %d OFFSET %d"; $where_values[] = $limit; $where_values[] = $offset; $results = $wpdb->get_results( $wpdb->prepare( $query, $where_values ), ARRAY_A ); // Get total count for pagination $count_query = "SELECT COUNT(*) FROM {$wpdb->prefix}kc_appointments a 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}"; // Remove limit and offset from where_values for count query $count_where_values = $where_values; array_pop( $count_where_values ); array_pop( $count_where_values ); $total = (int) $wpdb->get_var( $wpdb->prepare( $count_query, $count_where_values ) ); return array( 'appointments' => array_map( function( $appointment ) { $appointment['id'] = (int) $appointment['id']; $appointment['status_label'] = self::get_status_label( $appointment['status'] ); return $appointment; }, $results ), 'total' => $total, 'has_more' => ( $offset + $limit ) < $total ); } /** * Get doctor availability for date range * * @param int $doctor_id Doctor ID * @param string $start_date Start date * @param string $end_date End date * @return array Available slots * @since 1.0.0 */ public static function get_doctor_availability( $doctor_id, $start_date, $end_date ) { global $wpdb; // Get doctor schedule $doctor_schedule = get_option( "kivicare_doctor_{$doctor_id}_schedule", array() ); if ( empty( $doctor_schedule ) ) { return array(); } // Get existing appointments in date range $existing_appointments = $wpdb->get_results( $wpdb->prepare( "SELECT appointment_start_date, appointment_start_time, appointment_end_time, duration FROM {$wpdb->prefix}kc_appointments WHERE doctor_id = %d AND appointment_start_date BETWEEN %s AND %s AND status NOT IN (%d, %d)", $doctor_id, $start_date, $end_date, self::STATUS_CANCELLED, self::STATUS_NO_SHOW ), ARRAY_A ); // Calculate available slots $available_slots = array(); $current_date = new \DateTime( $start_date ); $end_date_obj = new \DateTime( $end_date ); while ( $current_date <= $end_date_obj ) { $day_name = strtolower( $current_date->format( 'l' ) ); if ( isset( $doctor_schedule[$day_name] ) && ! isset( $doctor_schedule[$day_name]['closed'] ) ) { $day_slots = self::calculate_day_slots( $current_date->format( 'Y-m-d' ), $doctor_schedule[$day_name], $existing_appointments ); if ( ! empty( $day_slots ) ) { $available_slots[$current_date->format( 'Y-m-d' )] = $day_slots; } } $current_date->add( new \DateInterval( 'P1D' ) ); } return $available_slots; } /** * Generate unique appointment number * * @return string Appointment number * @since 1.0.0 */ private static function generate_appointment_number() { global $wpdb; $date_prefix = current_time( 'Ymd' ); // Get the highest existing appointment number for today $max_number = $wpdb->get_var( $wpdb->prepare( "SELECT MAX(CAST(SUBSTRING(appointment_number, 9) AS UNSIGNED)) FROM {$wpdb->prefix}kc_appointments WHERE appointment_number LIKE %s", $date_prefix . '%' ) ); $next_number = ( $max_number ? $max_number + 1 : 1 ); return $date_prefix . str_pad( $next_number, 4, '0', STR_PAD_LEFT ); } /** * Calculate appointment end time * * @param string $start_time Start time * @param int $duration Duration in minutes * @return string End time * @since 1.0.0 */ private static function calculate_end_time( $start_time, $duration ) { $start_datetime = new \DateTime( $start_time ); $start_datetime->add( new \DateInterval( "PT{$duration}M" ) ); return $start_datetime->format( 'H:i:s' ); } /** * Validate appointment business rules * * @param array $appointment_data Appointment data * @param int $appointment_id Appointment ID (for updates) * @return bool|WP_Error True if valid, WP_Error otherwise * @since 1.0.0 */ private static function validate_appointment_business_rules( $appointment_data, $appointment_id = null ) { $errors = array(); // Validate required fields $required_fields = array( 'patient_id', 'doctor_id', 'clinic_id', 'appointment_start_date', 'appointment_start_time' ); foreach ( $required_fields as $field ) { if ( empty( $appointment_data[$field] ) ) { $errors[] = "Field {$field} is required"; } } // Validate appointment date is not in the past if ( ! empty( $appointment_data['appointment_start_date'] ) ) { $appointment_date = new \DateTime( $appointment_data['appointment_start_date'] ); $today = new \DateTime(); $today->setTime( 0, 0, 0 ); if ( $appointment_date < $today ) { $errors[] = 'Appointment date cannot be in the past'; } } // Validate patient exists if ( ! empty( $appointment_data['patient_id'] ) ) { global $wpdb; $patient_exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}kc_patients WHERE id = %d", $appointment_data['patient_id'] ) ); if ( ! $patient_exists ) { $errors[] = 'Invalid patient ID'; } } // Validate doctor exists if ( ! empty( $appointment_data['doctor_id'] ) ) { global $wpdb; $doctor_exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}kc_doctors WHERE id = %d", $appointment_data['doctor_id'] ) ); if ( ! $doctor_exists ) { $errors[] = 'Invalid doctor ID'; } } // Validate clinic exists if ( ! empty( $appointment_data['clinic_id'] ) ) { global $wpdb; $clinic_exists = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}kc_clinics WHERE id = %d", $appointment_data['clinic_id'] ) ); if ( ! $clinic_exists ) { $errors[] = 'Invalid clinic ID'; } } if ( ! empty( $errors ) ) { return new \WP_Error( 'appointment_business_validation_failed', 'Appointment business validation failed', array( 'status' => 400, 'errors' => $errors ) ); } return true; } /** * Check doctor availability for appointment slot * * @param array $appointment_data Appointment data * @param int $exclude_id Appointment ID to exclude (for updates) * @return bool|WP_Error True if available, WP_Error otherwise * @since 1.0.0 */ private static function check_doctor_availability( $appointment_data, $exclude_id = null ) { global $wpdb; $doctor_id = $appointment_data['doctor_id']; $start_date = $appointment_data['appointment_start_date']; $start_time = $appointment_data['appointment_start_time']; $duration = $appointment_data['duration'] ?? 30; $end_time = self::calculate_end_time( $start_time, $duration ); // Check for conflicting appointments $conflict_query = "SELECT id FROM {$wpdb->prefix}kc_appointments WHERE doctor_id = %d AND appointment_start_date = %s AND status NOT IN (%d, %d) AND ( (appointment_start_time <= %s AND appointment_end_time > %s) OR (appointment_start_time < %s AND appointment_end_time >= %s) OR (appointment_start_time >= %s AND appointment_end_time <= %s) )"; $conflict_params = array( $doctor_id, $start_date, self::STATUS_CANCELLED, self::STATUS_NO_SHOW, $start_time, $start_time, $end_time, $end_time, $start_time, $end_time ); if ( $exclude_id ) { $conflict_query .= " AND id != %d"; $conflict_params[] = $exclude_id; } $conflict = $wpdb->get_var( $wpdb->prepare( $conflict_query, $conflict_params ) ); if ( $conflict ) { return new \WP_Error( 'doctor_not_available', 'Doctor is not available at the selected time slot', array( 'status' => 400 ) ); } // Check doctor working hours $doctor_schedule = get_option( "kivicare_doctor_{$doctor_id}_schedule", array() ); $day_name = strtolower( date( 'l', strtotime( $start_date ) ) ); if ( empty( $doctor_schedule[$day_name] ) || isset( $doctor_schedule[$day_name]['closed'] ) ) { return new \WP_Error( 'doctor_not_working', 'Doctor is not working on this day', array( 'status' => 400 ) ); } $working_hours = $doctor_schedule[$day_name]; if ( $start_time < $working_hours['start_time'] || $end_time > $working_hours['end_time'] ) { return new \WP_Error( 'outside_working_hours', 'Appointment time is outside doctor working hours', array( 'status' => 400 ) ); } // Check break time if exists if ( isset( $working_hours['break_start'] ) && isset( $working_hours['break_end'] ) ) { $break_start = $working_hours['break_start']; $break_end = $working_hours['break_end']; if ( ( $start_time >= $break_start && $start_time < $break_end ) || ( $end_time > $break_start && $end_time <= $break_end ) || ( $start_time <= $break_start && $end_time >= $break_end ) ) { return new \WP_Error( 'during_break_time', 'Appointment time conflicts with doctor break time', array( 'status' => 400 ) ); } } return true; } /** * Additional helper methods for appointment management */ private static function is_appointment_rescheduling( $current_appointment, $new_data ) { return ( isset( $new_data['appointment_start_date'] ) && $new_data['appointment_start_date'] != $current_appointment['appointment_start_date'] ) || ( isset( $new_data['appointment_start_time'] ) && $new_data['appointment_start_time'] != $current_appointment['appointment_start_time'] ) || ( isset( $new_data['doctor_id'] ) && $new_data['doctor_id'] != $current_appointment['doctor_id'] ); } private static function handle_status_changes( $appointment_id, $current_appointment, $new_data ) { if ( isset( $new_data['status'] ) && $new_data['status'] != $current_appointment['status'] ) { $status_change = array( 'appointment_id' => $appointment_id, 'from_status' => $current_appointment['status'], 'to_status' => $new_data['status'], 'changed_by' => get_current_user_id(), 'changed_at' => current_time( 'mysql' ) ); do_action( 'kivicare_appointment_status_changed', $status_change ); } } private static function setup_appointment_defaults( $appointment_id, $appointment_data ) { // Setup any default values or related data update_option( "kivicare_appointment_{$appointment_id}_created", current_time( 'mysql' ) ); } private static function send_appointment_notifications( $appointment_id, $type ) { // Send notifications to patient, doctor, etc. do_action( "kivicare_send_appointment_{$type}_notification", $appointment_id ); } private static function auto_generate_bill( $appointment_id ) { // Auto-generate bill for completed appointment do_action( 'kivicare_auto_generate_bill', $appointment_id ); } private static function get_appointment_patient( $patient_id ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( "SELECT id, first_name, last_name, user_email, contact_no FROM {$wpdb->prefix}kc_patients WHERE id = %d", $patient_id ), ARRAY_A ); } private static function get_appointment_doctor( $doctor_id ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( "SELECT id, first_name, last_name, user_email, mobile_number, specialties FROM {$wpdb->prefix}kc_doctors WHERE id = %d", $doctor_id ), ARRAY_A ); } private static function get_appointment_clinic( $clinic_id ) { global $wpdb; return $wpdb->get_row( $wpdb->prepare( "SELECT id, name, address, city, telephone_no FROM {$wpdb->prefix}kc_clinics WHERE id = %d", $clinic_id ), ARRAY_A ); } private static function get_appointment_service( $service_id ) { if ( ! $service_id ) return null; global $wpdb; return $wpdb->get_row( $wpdb->prepare( "SELECT id, name, price, duration FROM {$wpdb->prefix}kc_services WHERE id = %d", $service_id ), ARRAY_A ); } private static function get_appointment_encounters( $appointment_id ) { global $wpdb; return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}kc_encounters WHERE appointment_id = %d ORDER BY encounter_date DESC", $appointment_id ), ARRAY_A ); } private static function get_appointment_bills( $appointment_id ) { global $wpdb; return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}kc_bills WHERE appointment_id = %d ORDER BY created_at DESC", $appointment_id ), ARRAY_A ); } private static function get_status_label( $status ) { $labels = array( self::STATUS_BOOKED => 'Booked', self::STATUS_COMPLETED => 'Completed', self::STATUS_CANCELLED => 'Cancelled', self::STATUS_NO_SHOW => 'No Show', self::STATUS_RESCHEDULED => 'Rescheduled' ); return $labels[$status] ?? 'Unknown'; } private static function calculate_day_slots( $date, $schedule, $existing_appointments ) { $slots = array(); $slot_duration = 30; // minutes $start_time = new \DateTime( $date . ' ' . $schedule['start_time'] ); $end_time = new \DateTime( $date . ' ' . $schedule['end_time'] ); // Handle break time $break_start = isset( $schedule['break_start'] ) ? new \DateTime( $date . ' ' . $schedule['break_start'] ) : null; $break_end = isset( $schedule['break_end'] ) ? new \DateTime( $date . ' ' . $schedule['break_end'] ) : null; $current_time = clone $start_time; while ( $current_time < $end_time ) { $slot_end = clone $current_time; $slot_end->add( new \DateInterval( "PT{$slot_duration}M" ) ); // Skip if in break time if ( $break_start && $break_end && ( $current_time >= $break_start && $current_time < $break_end ) ) { $current_time = clone $break_end; continue; } // Check if slot is available $is_available = true; foreach ( $existing_appointments as $appointment ) { if ( $appointment['appointment_start_date'] == $date ) { $app_start = new \DateTime( $date . ' ' . $appointment['appointment_start_time'] ); $app_end = new \DateTime( $date . ' ' . $appointment['appointment_end_time'] ); if ( ( $current_time >= $app_start && $current_time < $app_end ) || ( $slot_end > $app_start && $slot_end <= $app_end ) || ( $current_time <= $app_start && $slot_end >= $app_end ) ) { $is_available = false; break; } } } if ( $is_available ) { $slots[] = array( 'start_time' => $current_time->format( 'H:i:s' ), 'end_time' => $slot_end->format( 'H:i:s' ), 'duration' => $slot_duration ); } $current_time->add( new \DateInterval( "PT{$slot_duration}M" ) ); } return $slots; } /** * Event handlers */ public static function on_appointment_created( $appointment_id, $appointment_data ) { error_log( "Care: New appointment created - ID: {$appointment_id}, Patient: " . ( $appointment_data['patient_id'] ?? 'Unknown' ) ); } public static function on_appointment_updated( $appointment_id, $appointment_data ) { error_log( "Care: Appointment updated - ID: {$appointment_id}" ); wp_cache_delete( "appointment_{$appointment_id}", 'kivicare' ); } public static function on_appointment_cancelled( $appointment_id ) { error_log( "Care: Appointment cancelled - ID: {$appointment_id}" ); wp_cache_delete( "appointment_{$appointment_id}", 'kivicare' ); } public static function on_appointment_completed( $appointment_id ) { error_log( "Care: Appointment completed - ID: {$appointment_id}" ); wp_cache_delete( "appointment_{$appointment_id}", 'kivicare' ); } }