calendar = new Calendar(); $this->appointment_api = new Api_Appointment(); // Add hooks add_action( 'timetics_after_booking_schedule', array( $this, 'sync_booking_to_google_calendar' ), 10, 4 ); add_filter( 'timetics/admin/booking/get_items', array( $this, 'get_events_from_google' ) ); add_filter( 'timetics_schedule_data_for_selected_date', array( $this, 'block_timeslots_by_google_events' ), 10, 5 ); } catch ( \Throwable $e ) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging is properly guarded by WP_DEBUG checks error_log( 'Timetics Google Calendar Sync: ' . $e->getMessage() ); } } } /** * Get all Google Event IDs that were created by Timetics * * @return array */ private function get_timetics_google_event_ids() { $booking = new Booking(); return $booking->get_all_google_event_ids(); } /** * Sync booking to Google Calendar * * @param int $booking_id Booking ID * @param int $customer_id Customer ID * @param int $meeting_id Meeting ID * @param array $data Booking data * * @return void|WP_Error */ public function sync_booking_to_google_calendar( $booking_id, $customer_id, $meeting_id, $data ) { try { if ( ! is_numeric( $booking_id ) || ! is_numeric( $customer_id ) || ! is_numeric( $meeting_id ) ) { return new WP_Error( 'invalid_parameters', 'Invalid parameters provided for Google Calendar sync.' ); } $booking = new Booking( $booking_id ); $meeting = new Appointment( $meeting_id ); $customer = new Customer( $customer_id ); // Get the current user's access token $user_id = get_current_user_id(); $access_token = timetics_get_google_access_token( $user_id ); if ( empty( $access_token ) ) { return new WP_Error( 'no_access_token', 'No Google Calendar access token found. Please reconnect your Google account.' ); } // Prepare event data $event_data = array( 'access_token' => sanitize_text_field( $access_token ), 'summary' => sanitize_text_field( $meeting->get_name() ), 'description' => sanitize_text_field( $meeting->get_description() ), 'start' => array( 'date' => $booking->get_start_date(), 'time' => $booking->get_start_time(), ), 'end' => array( 'date' => $booking->get_end_date(), 'time' => $booking->get_end_time(), ), 'attendees' => array( array( 'email' => $customer->get_email() ), ), 'reminders' => array( 'useDefault' => true, ), 'guestsCanInviteOthers' => false, 'guestsCanModify' => false, 'guestsCanSeeOtherGuests' => false, 'timezone' => $booking->get_timezone(), ); // Check if this booking already has a Google Event ID $event_id = $booking->get_google_event_id(); if ( $event_id ) { // Update existing event $result = $this->calendar->update_event( $event_id, $event_data ); } else { // Create new event $result = $this->calendar->create_event( $event_data ); // Save the event ID for future updates if ( ! empty( $result['id'] ) ) { $booking->set_google_event_id( $result['id'] ); } } if ( is_wp_error( $result ) ) { $booking->set_sync_status( 'error' ); return $result; } $booking->set_sync_status( 'synced' ); } catch ( \Throwable $e ) { // If sync fails silenty exits to ensure no other process gets hampered if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging is properly guarded by WP_DEBUG checks error_log( 'Timetics Google Calendar Sync: ' . $e->getMessage() ); } } } /** * Get events from Google Calendar * Only returns events that were not created by Timetics * * @param array $bookings Existing bookings array * * @return array Modified bookings array with Google Calendar events */ public function get_events_from_google( $bookings ) { try { // Get Google Calendar events $events = $this->calendar->get_events( get_current_user_id() ); if ( is_wp_error( $events ) || empty( $events ) ) { return $bookings; } // Get all Timetics-created Google Event IDs $timetics_event_ids = $this->get_timetics_google_event_ids(); $google_events = array(); foreach ( $events as $event ) { // Skip events that were created by Timetics if ( in_array( $event['id'] ?? null, $timetics_event_ids, true ) ) { continue; } // mapping google events data to timetics booking format $google_events[] = array( 'id' => $event['id'] ?? '', 'order_total' => '', 'title' => $event['summary'] ?? '', 'description' => $event['description'] ?? '', 'date' => $event['start_date'] ?? '', 'start_date' => $event['start_date'] ?? '', 'end_date' => $event['end_date'] ?? '', 'start_time' => $event['start_time'] ?? '', 'end_time' => $event['end_time'] ?? '', 'source' => 'google', 'status' => 'approved', 'random_id' => 'I' . ( $event['id'] ?? '' ), 'appointment' => array( 'id' => $event['id'] ?? '', 'name' => $event['summary'] ?? '', 'timezone' => $event['timezone'] ?? '', ), ); } // Merge with existing bookings return array_merge( $bookings, $google_events ); } catch ( \Throwable $e ) { return $bookings; } } /** * Block timeslots by Google events * * @param array $data * @param int $staff_id * @param int $meeting_id * @param string $timezone * * @return array Modified bookings array with Google Calendar events */ public function block_timeslots_by_google_events( $data, $staff_id, $meeting_id, $timezone ) { try { if ( ! timetics_get_option( 'google_calendar_overlap', false ) ) { return $data; } if ( empty( $data ) ) { return $data; } // Fetch Google events for the full date range in one request $first_day = reset( $data ); $last_day = end( $data ); $date_obj_start = new DateTime( $first_day['date'] . ' 00:00:00', new DateTimeZone( $timezone ) ); $date_obj_end = new DateTime( $last_day['date'] . ' 23:59:59', new DateTimeZone( $timezone ) ); $google_events = $this->calendar->get_events( $staff_id, [ 'timeMin' => rawurlencode( $date_obj_start->format( DateTime::RFC3339 ) ), 'timeMax' => rawurlencode( $date_obj_end->format( DateTime::RFC3339 ) ), 'orderBy' => 'startTime', 'singleEvents' => 'true', 'timeZone' => $timezone, ]); // if theres no google calendar event for the staff member, return the data as it is if ( is_wp_error( $google_events ) || empty( $google_events ) ) { return $data; } // Index events by date for efficient per-day lookup $events_by_date = []; foreach ( $google_events as $event ) { $event_date = $event['start_date'] ?? ''; if ( $event_date ) { $events_by_date[ $event_date ][] = $event; } } // Process each day in the range foreach ( $data as $index => $time_slot_data ) { $date = $time_slot_data['date']; if ( empty( $events_by_date[ $date ] ) ) { continue; } $day_events = $events_by_date[ $date ]; $updated_slots = []; foreach ( $time_slot_data['slots'] as $slot ) { $slot_start_time = strtotime( $slot['start_time'] ); // Check against each Google Calendar event for this day foreach ( $day_events as $event ) { $event_start = strtotime( $event['start_time'] ); $event_end = strtotime( $event['end_time'] ); // If the slot overlaps with the event, mark it as unavailable if ( $slot_start_time >= $event_start && $slot_start_time < $event_end ) { $slot['status'] = 'unavailable'; break; } } if ( $slot['status'] === 'unavailable' ) { continue; } $updated_slots[] = $slot; } $data[ $index ]['slots'] = $updated_slots; } return $data; } catch (\Throwable $e) { // Silenty reverts to normal behavior incase of error return $data; } } }