guard_staff(); if ( $check ) { return $check; } try { $days = min( 90, max( 1, (int) ( $days_ahead ?? self::DEFAULT_DAYS ) ) ); $start_date = current_time( 'Y-m-d' ); $end_date = gmdate( 'Y-m-d', strtotime( $start_date . ' +' . $days . ' days' ) ); $params = [ 'start_date' => $start_date, 'end_date' => $end_date, ]; if ( null !== $service_id ) { $params['service_id'] = $service_id; } $scoped_team_member = $this->enforced_team_member_id(); if ( null !== $scoped_team_member ) { $params['team_member_id'] = $scoped_team_member; } elseif ( null !== $team_member_id ) { $params['team_member_id'] = $team_member_id; } $request = new \WP_REST_Request( 'GET', '/booktics/v1/appointments/calendar' ); $request->set_query_params( $params ); $controller = new \Booktics\Appointment\Controllers\Appointment_Controller(); $result = $this->extract_response( $controller->get_all_calendar_items( $request ), 'Failed to retrieve upcoming bookings.' ); if ( isset( $result['error'] ) ) { return $result; } if ( ! isset( $result['appointments'] ) || ! is_array( $result['appointments'] ) ) { return [ 'error' => 'The booking schedule could not be read (unexpected calendar response). Please try again.' ]; } $appointments = $result['appointments']; // Defence-in-depth staff scoping (the calendar endpoint has the same // OR-combined team_member_id caveat as the list endpoint). $appointments = $this->filter_to_own_appointments( $appointments ); // Status must be filtered here: the calendar endpoint filters `status` // as post meta, but appointment status is stored as post_status, so a // server-side status filter never matches. $unfiltered_count = count( $appointments ); $status_removed_all = false; if ( ! empty( $status ) ) { $appointments = array_values( array_filter( $appointments, static function ( $row ) use ( $status ) { return is_array( $row ) && (string) ( $row['status'] ?? '' ) === $status; } ) ); // Distinguish a typo'd status from a genuinely empty result. $status_removed_all = 0 === count( $appointments ) && $unfiltered_count > 0; } $total = count( $appointments ); $truncated = $total > self::MAX_RESULTS; if ( $truncated ) { $appointments = array_slice( $appointments, 0, self::MAX_RESULTS ); } $response = [ 'date_range' => [ 'start' => $start_date, 'end' => $end_date, ], 'total' => $total, 'items' => $this->shape_rows( $appointments, fn( $appointment ) => $this->shape_appointment( $appointment ) ), ]; if ( $truncated ) { $response['note'] = sprintf( 'Showing the first %d of %d bookings in this range. Narrow the range or add filters to see more.', self::MAX_RESULTS, $total ); } elseif ( $status_removed_all ) { $response['note'] = sprintf( "No upcoming bookings matched status '%s'. %d booking(s) exist in this range with other statuses.", $status, $unfiltered_count ); } return $response; } catch ( \Throwable $e ) { Log::error( 'Booktics_Get_Upcoming_Bookings_Tool error', [ 'error' => $e->getMessage() ] ); return [ 'error' => 'Failed to retrieve upcoming bookings: ' . $e->getMessage() ]; } } }