namespace, $this->base, array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_item' ), 'permission_callback' => array( $this, 'create_item_permissions_check', ), ), ) ); register_rest_route( $this->namespace, $this->base, array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_items' ), 'permission_callback' => array( $this, 'get_items_permissions_check', ), ), ) ); register_rest_route( $this->namespace, $this->base, array( array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_items' ), 'permission_callback' => array( $this, 'delete_items_permissions_check', ), ), ) ); register_rest_route( $this->namespace, $this->base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_item' ), 'permission_callback' => array( $this, 'get_item_permissions_check', ), ), ) ); register_rest_route( $this->namespace, $this->base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_item' ), 'permission_callback' => array( $this, 'update_item_permissions_check', ), ), ) ); register_rest_route( $this->namespace, $this->base . '/(?P[\d]+)', array( array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array( $this, 'delete_item' ), 'permission_callback' => array( $this, 'delete_item_permissions_check', ), ), ) ); register_rest_route( $this->namespace, $this->base . '/(?P[\d]+)/reschedule', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'reschedule_item' ), 'permission_callback' => array( $this, 'reschedule_item_permissions_check', ), ), ) ); register_rest_route( $this->namespace, $this->base . '/(?P[\d]+)/cancel', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'cancel_item' ), 'permission_callback' => array( $this, 'cancel_item_permissions_check', ), ), ) ); register_rest_route( $this->namespace, $this->base . '/time-slots', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_time_slots' ), 'permission_callback' => array( $this, 'get_time_slots_permissions_check', ), ), ) ); register_rest_route( $this->namespace, $this->base . '/calendar', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_all_calendar_items', ), 'permission_callback' => array( $this, 'get_items_permissions_check', ), 'args' => array( 'month' => array( 'description' => __( 'Month in YYYY-MM format', 'booktics' ), 'type' => 'string', 'validate_callback' => function ( $param ) { return preg_match( '/^\d{4}-\d{2}$/', $param ); }, ), ), ), ) ); } /** * Get a collection of items. * * @param WP_REST_Request $request Full data about the request. * * @return WP_HTTP_Response */ public function get_items( $request ) { $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 10; $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1; $date_from = ! empty( $request['date_from'] ) ? sanitize_text_field( $request['date_from'] ) : null; $service_id = ! empty( $request['service_id'] ) ? intval( $request['service_id'] ) : null; $customer_id = ! empty( $request['customer_id'] ) ? intval( $request['customer_id'] ) : null; $payment_status = ! empty( $request['payment_status'] ) ? sanitize_text_field( $request['payment_status'] ) : null; $team_member_id = ! empty( $request['team_member_id'] ) ? intval( $request['team_member_id'] ) : null; $status = ! empty( $request['status'] ) ? sanitize_text_field( $request['status'] ) : null; $search = ! empty( $request['search'] ) ? sanitize_text_field( $request['search'] ) : null; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering appointments by meta fields $args = array( 'meta_query' => array() ); if ( $search ) { $service_ids = array(); $customer_ids = array(); $team_member_ids = array(); // Search Services $services = Service_Model::all( array( 's' => $search, ) ); $service_ids = array_map( function ( $service ) { return $service->id; }, $services ); // Search Customers $customer_query = new \WP_User_Query( array( 'search' => '*' . esc_attr( $search ) . '*', 'search_columns' => array( 'display_name', 'user_email' ), 'role' => 'booktics_customer', 'fields' => 'ID', ) ); $customer_ids = $customer_query->get_results(); // Search Team Members $team_member_query = new \WP_User_Query( array( 'search' => '*' . esc_attr( $search ) . '*', 'search_columns' => array( 'display_name', 'user_email' ), 'role' => 'booktics_team_member', 'fields' => 'ID', ) ); $team_member_ids = $team_member_query->get_results(); if ( $service_ids || $customer_ids || $team_member_ids ) { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering users by meta fields $args['meta_query'] = array( 'relation' => 'OR' ); if ( $service_ids ) { $args['meta_query'][] = array( 'key' => 'service_id', 'value' => $service_ids, 'compare' => 'IN', ); } if ( $customer_ids ) { $args['meta_query'][] = array( 'key' => 'customer_id', 'value' => $customer_ids, 'compare' => 'IN', ); } if ( $team_member_ids ) { $args['meta_query'][] = array( 'key' => 'team_member_id', 'value' => $team_member_ids, 'compare' => 'IN', ); } } else { return $this->response( array( 'items' => array(), 'total' => 0, 'per_page' => $per_page, 'current_page' => $paged, 'last_page' => 0, ), __( 'No appointments found', 'booktics' ) ); } } if ( $date_from ) { $args['meta_query'][] = array( 'key' => 'date', 'value' => $date_from, 'compare' => '>=', 'type' => 'DATE', ); } if ( $service_id ) { $args['meta_query'][] = array( 'key' => 'service_id', 'value' => $service_id, 'compare' => '=', ); } if ( $customer_id ) { $args['meta_query'][] = array( 'key' => 'customer_id', 'value' => $customer_id, 'compare' => '=', ); } if ( $payment_status ) { $args['meta_query'][] = array( 'key' => 'payment_status', 'value' => $payment_status, 'compare' => '=', ); } if ( $team_member_id ) { $args['meta_query'][] = array( 'key' => 'team_member_id', 'value' => $team_member_id, 'compare' => '=', ); } // Adds filtering by appointment status if ( $status ) { $args['post_status'] = is_array( $status ) ? $status : array( $status ); } $args = apply_filters( 'booktics_appointment_args', $args ); // Scope to customer's own appointments when not admin/team-member if ( ! booktics_is_admin_or_team_member() && is_user_logged_in() ) { $db = new Booktics_Database(); $guest = ( new Guest_Model( $db ) )->find( array( 'wp_user_id' => get_current_user_id() ) ); if ( ! $guest ) { return $this->response( array( 'items' => array(), 'total' => 0, 'per_page' => $per_page, 'current_page' => $paged, 'last_page' => 0, ), __( 'No appointments found', 'booktics' ) ); } // Add customer_id filter to meta_query $args['meta_query'][] = array( 'key' => 'customer_id', 'value' => $guest->id, 'compare' => '=', ); } $data = Appointment_Model::paginate( $per_page, $paged, $args ); $data = apply_filters( 'booktics_appointment_data', $data ); // Mask team member email for customers (non-admin/team-member) if ( ! booktics_is_admin_or_team_member() && is_user_logged_in() && ! empty( $data['items'] ) ) { foreach ( $data['items'] as &$appointment ) { if ( isset( $appointment->team_member_id ) ) { $team_member = new Team_Member_Model( $appointment->team_member_id ); $team_member_data = $team_member->find( $appointment->team_member_id ); if ( $team_member_data ) { // Remove user_email from team data to protect privacy $team_member_data->user_email = ''; } } } } if ( ! $data ) { return $this->error( __( 'No appointment found', 'booktics' ), 404 ); } return $this->response( $data ); } /** * Get one item from the collection. * * @param WP_REST_Request $request Full data about the request. * * @return WP_Error|WP_REST_Response */ public function get_item( $request ) { $id = intval( $request['id'] ); $appointment = Appointment_Model::find( $id ); if ( ! $appointment ) { return $this->error( __( 'Appointment not found', 'booktics' ), 404 ); } // Mask team member email for customers (non-admin/team-member) if ( ! booktics_is_admin_or_team_member() && is_user_logged_in() ) { $appointment_array = is_array( $appointment ) ? $appointment : ( is_object( $appointment ) ? get_object_vars( $appointment ) : array() ); if ( isset( $appointment_array['team_member_id'] ) ) { $team_member = new Team_Member_Model( $appointment_array['team_member_id'] ); $team_member_data = $team_member->find( $appointment_array['team_member_id'] ); if ( $team_member_data && isset( $team_member_data->user_email ) ) { // We can't easily unset from the object, but we can return a modified response } } } return $this->response( $appointment ); } /** * Get all calendar items within a date range * * @param WP_REST_Request $request Full data about the request. * * @return WP_Error|WP_REST_Response */ public function get_all_calendar_items( $request ) { $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 500; $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1; // Check if custom date range is provided if ( ! empty( $request['start_date'] ) && ! empty( $request['end_date'] ) ) { $adjusted_start_date = sanitize_text_field( $request['start_date'] ); $adjusted_end_date = sanitize_text_field( $request['end_date'] ); $month = gmdate( 'Y-m', strtotime( $adjusted_start_date ) ); } else { // Fall back to month-based date range with week adjustment $month = ! empty( $request['month'] ) ? sanitize_text_field( $request['month'] ) : gmdate( 'Y-m' ); $start_date = gmdate( 'Y-m-01', strtotime( $month ) ); $end_date = gmdate( 'Y-m-t', strtotime( $month ) ); // Get the first day of the week (Sunday = 0, Monday = 1, etc.) $first_day_of_week = get_option( 'start_of_week', 1 ); // Adjust start date to include days from previous month to complete the first week $start_date_obj = new \DateTime( $start_date ); $start_week_day = (int) $start_date_obj->format( 'w' ); // 0 (Sun) to 6 (Sat) $days_to_subtract = ( $start_week_day - $first_day_of_week + 7 ) % 7; $start_date_obj->modify( "-$days_to_subtract days" ); $adjusted_start_date = $start_date_obj->format( 'Y-m-d' ); // Adjust end date to include days from next month to complete the last week $end_date_obj = new \DateTime( $end_date ); $end_week_day = (int) $end_date_obj->format( 'w' ); // 0 (Sun) to 6 (Sat) $days_to_add = ( 6 - $end_week_day + $first_day_of_week ) % 7; $end_date_obj->modify( "+$days_to_add days" ); $adjusted_end_date = $end_date_obj->format( 'Y-m-d' ); } // Get all appointments within the date range using meta_query $args = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering appointments by meta fields 'meta_query' => array( 'relation' => 'AND', array( 'key' => 'date', 'value' => array( $adjusted_start_date, $adjusted_end_date, ), 'compare' => 'BETWEEN', 'type' => 'DATE', ), ), 'posts_per_page' => - 1, // Get all appointments in the range 'orderby' => 'meta_value', 'meta_key' => 'date', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Required for ordering by date meta field 'order' => 'ASC', ); $service_id = ! empty( $request['service_id'] ) ? intval( $request['service_id'] ) : null; $customer_id = ! empty( $request['customer_id'] ) ? intval( $request['customer_id'] ) : null; $payment_status = ! empty( $request['payment_status'] ) ? sanitize_text_field( $request['payment_status'] ) : null; $team_member_id = ! empty( $request['team_member_id'] ) ? intval( $request['team_member_id'] ) : null; $status = ! empty( $request['status'] ) ? sanitize_text_field( $request['status'] ) : null; if ( $service_id ) { $args['meta_query'][] = array( 'key' => 'service_id', 'value' => $service_id, 'compare' => '=', ); } if ( $customer_id ) { $args['meta_query'][] = array( 'key' => 'customer_id', 'value' => $customer_id, 'compare' => '=', ); } if ( $payment_status ) { $args['meta_query'][] = array( 'key' => 'payment_status', 'value' => $payment_status, 'compare' => '=', ); } if ( $team_member_id ) { $args['meta_query'][] = array( 'key' => 'team_member_id', 'value' => $team_member_id, 'compare' => '=', ); } if ( $status ) { $args['meta_query'][] = array( 'key' => 'status', 'value' => $status, 'compare' => '=', ); } $args = apply_filters( 'booktics_appointment_args', $args ); $appointments = Appointment_Model::paginate( $per_page, $paged, $args ); // Format the response $formatted_appointments = array(); if ( ! empty( $appointments ) ) { foreach ( $appointments['items'] as $appointment ) { if ( ! empty( $appointment ) ) { $appointment->source = 'booktics'; } $formatted_appointments[] = $appointment; } } // Allow other developers to add/modify appointments $formatted_appointments = apply_filters( 'booktics_appointments_for_calendar_view', $formatted_appointments, array( 'start_date' => $adjusted_start_date, 'end_date' => $adjusted_end_date, ) ); $response = array( 'month' => gmdate( 'Y-m', strtotime( $month ) ), 'date_range' => array( 'start' => $adjusted_start_date, 'end' => $adjusted_end_date, ), 'appointments' => $formatted_appointments, ); return $this->response( $response, __( 'Calendar appointments retrieved successfully', 'booktics' ) ); } /** * Check if a given request has access to get a specific item. * * @param WP_REST_Request $request Full data about the request. * * @return WP_Error|boolean */ public function get_item_permissions_check( $request ) { if ( booktics_is_admin_or_team_member() ) { return true; } if ( is_user_logged_in() ) { $appointment_id = intval( $request['id'] ); $appointment = Appointment_Model::find( $appointment_id ); if ( $appointment ) { $db = new Booktics_Database(); $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $appointment->customer_id ) ); if ( $customer && intval( $customer->wp_user_id ) === get_current_user_id() ) { return true; } } } return new WP_Error( 'booktics_forbidden', __( 'You are not allowed to view this appointment.', 'booktics' ), array( 'status' => 403 ) ); } /** * Check if a given request has access to get items. * * @param WP_REST_Request $request Full data about the request. * * @return WP_Error|boolean */ public function get_items_permissions_check( $request ) { if ( booktics_is_admin_or_team_member() ) { return true; } if ( is_user_logged_in() ) { return true; } return new WP_Error( 'booktics_forbidden', __( 'You are not allowed to view appointments.', 'booktics' ), array( 'status' => 403 ) ); } /** * Create one item from the collection. * * @param WP_REST_Request $request Full data about the request. * * @return WP_Error|WP_REST_Response * @throws Exception */ public function create_item( $request ) { $data = json_decode( $request->get_body(), true ); $error = Appointment_Validator::instance()->validate_create( $data ); if ( $error ) { return $this->error( $error->get_error_message() ); } $appointment = Appointment_Service::instance()->create( $data ); if ( is_wp_error( $appointment ) ) { return $this->error( $appointment->get_error_message() ); } do_action( 'booktics_after_appointment_created', $appointment ); return $this->response( $appointment, __( 'Successfully created appointment', 'booktics' ) ); } /** * Check if a given request has access to create items. * * @param WP_REST_Request $request Full data about the request. * * @return WP_Error|boolean */ public function create_item_permissions_check( $request ) { return current_user_can( 'manage_options' ); } /** * Update one item from the collection. * * @param WP_REST_Request $request Full data about the request. * * @return WP_REST_Response */ public function update_item( $request ) { $id = intval( $request['id'] ); $data = json_decode( $request->get_body(), true ); $error = Appointment_Validator::instance()->validate_update( $data ); if ( $error ) { return $this->error( $error->get_error_message() ); } $updated_appointment = Appointment_Service::instance()->update( $id, $data ); if ( is_wp_error( $updated_appointment ) ) { return $this->error( $updated_appointment->get_error_message() ); } do_action( 'booktics_after_appointment_updated', $updated_appointment ); return $this->response( $updated_appointment, __( 'Appointment updated successfully', 'booktics' ) ); } /** * Check if a given request has access to update a specific item. * * @param WP_REST_Request $request Full data about the request. * * @return WP_Error|boolean */ public function update_item_permissions_check( $request ) { return current_user_can( 'manage_options' ); } /** * Delete one item from the collection. * * @param WP_REST_Request $request Full data about the request. * * @return \WP_Error|\WP_HTTP_Response */ public function delete_item( $request ) { $id = intval( $request['id'] ); $result = Appointment_Service::instance()->delete( $id ); if ( is_wp_error( $result ) ) { return $this->error( $result->get_error_message() ); } do_action( 'booktics_after_appointment_deleted', $result ); return $this->response( $result, __( 'Successfully deleted appointment', 'booktics' ) ); } /** * Check if a given request has access to delete a specific item. * * @param WP_REST_Request $request Full data about the request. * * @return WP_Error|boolean */ public function delete_item_permissions_check( $request ) { return current_user_can( 'manage_options' ); } /** * Delete a collection of items * * @param WP_Rest_Request $request User request */ public function delete_items( $request ) { $ids = $request['ids']; $result = Appointment_Service::instance()->delete_many( $ids ); if ( is_wp_error( $result ) ) { return $this->error( $result->get_error_message() ); } return $this->response( $result['data'], $result['message'] ); } /** * Check if a given request has access to delete a collection of items. * * @param WP_REST_Request $request Full data about the request. * * @return WP_Error|boolean */ public function delete_items_permissions_check( $request ) { return current_user_can( 'manage_options' ); } /** * Reschedule an appointment * * @param $request Full data about the request. * * @throws Exception */ public function reschedule_item( $request ) { $id = intval( $request['id'] ); $data = json_decode( $request->get_body(), true ); $error = Appointment_Validator::instance()->validate_reschedule( $data, $id ); if ( $error ) { return $this->error( $error->get_error_message() ); } $result = Appointment_Service::instance()->reschedule( $id, $data ); if ( is_wp_error( $result ) ) { return $this->error( $result->get_error_message() ); } $this->send_reschedule_email( $result ); do_action( 'booktics_after_appointment_rescheduled', $result ); return $this->response( $result, __( 'Successfully rescheduled appointment', 'booktics' ) ); } /** * Check if a given request has access to reschedule a specific item. * * @param $request Full data about the request. */ public function reschedule_item_permissions_check( $request ) { if ( booktics_is_admin_or_team_member() ) { return true; } if ( is_user_logged_in() ) { $appointment_id = intval( $request['id'] ); $appointment = Appointment_Model::find( $appointment_id ); if ( $appointment ) { $db = new Booktics_Database(); $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $appointment->customer_id ) ); if ( $customer && intval( $customer->wp_user_id ) === get_current_user_id() ) { return true; } } } return new WP_Error( 'booktics_forbidden', __( 'You are not allowed to reschedule this appointment.', 'booktics' ), array( 'status' => 403 ) ); } /** * Send reschedule email to customer * * @param Appointment_Model $appointment Appointment model. */ private function send_reschedule_email( $appointment ) { $db = new Booktics_Database(); $customer = ( new Guest_Model( $db ) )->find( [ 'id' => $appointment->customer_id ] ); $team_member = new Team_Member_Model( $appointment->team_member_id ); $appointment_reschedule_email_data = [ 'date' => $appointment->date ?? '', 'start_time' => $appointment->start_time ?? '', 'end_time' => $appointment->end_time ?? '', 'customer_name' => $customer->name ?? '', 'user_email' => $customer->email ?? '', 'appointment_service' => $appointment->title ?? '', 'appointment_team_member' => $team_member->display_name ?? '', 'appointment_team_member_email' => $team_member->user_email ?? '', ]; $appointment_reschedule_email_data = apply_filters( 'booktics_appointment_reschedule_email_data', $appointment_reschedule_email_data ); do_action( 'global_notification_hook', 'appointment_rescheduled', $appointment_reschedule_email_data ); } /** * Cancel an appointment * * @param WP_REST_Request $request Full data about the request. * * @throws Exception */ public function cancel_item( $request ) { $id = intval( $request['id'] ); $data = json_decode( $request->get_body(), true ); $cancel_note = ! empty( $data['cancel_note'] ) ? sanitize_text_field( $data['cancel_note'] ) : ''; $error = Appointment_Validator::instance()->validate_cancel( $id ); if ( $error ) { return $this->error( $error->get_error_message() ); } $result = Appointment_Service::instance()->cancel( $id, [ 'cancel_note' => $cancel_note ] ); if ( is_wp_error( $result ) ) { return $this->error( $result->get_error_message() ); } $this->send_cancel_email( $result ); do_action( 'booktics_after_appointment_cancelled', $result ); return $this->response( $result, __( 'Successfully cancelled appointment', 'booktics' ) ); } /** * Send cancel email to customer * * @param Appointment_Model $appointment Appointment model. */ private function send_cancel_email( $appointment ) { $db = new Booktics_Database(); $customer = ( new Guest_Model( $db ) )->find( [ 'id' => $appointment->customer_id ] ); $team_member = new Team_Member_Model( $appointment->team_member_id ); $appointment_cancel_email_data = [ 'date' => $appointment->date ?? '', 'start_time' => $appointment->start_time ?? '', 'end_time' => $appointment->end_time ?? '', 'customer_name' => $customer->name ?? '', 'user_email' => $customer->email ?? '', 'appointment_service' => $appointment->title ?? '', 'appointment_team_member' => $team_member->display_name ?? '', 'appointment_team_member_email' => $team_member->user_email ?? '', ]; $appointment_cancel_email_data = apply_filters( 'booktics_appointment_cancel_email_data', $appointment_cancel_email_data ); do_action( 'global_notification_hook', 'appointment_cancelled', $appointment_cancel_email_data ); } /** * Check if a given request has access to cancel a specific item. * * @param WP_REST_Request $request Full data about the request. * * @return WP_Error|boolean */ public function cancel_item_permissions_check( $request ) { if ( booktics_is_admin_or_team_member() ) { return true; } if ( is_user_logged_in() ) { $appointment_id = intval( $request['id'] ); $appointment = Appointment_Model::find( $appointment_id ); if ( $appointment ) { $db = new Booktics_Database(); $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $appointment->customer_id ) ); if ( $customer && intval( $customer->wp_user_id ) === get_current_user_id() ) { return true; } } } return new WP_Error( 'booktics_forbidden', __( 'You are not allowed to cancel this appointment.', 'booktics' ), array( 'status' => 403 ) ); } /** * Prepare the item for create or update operation. * * @param WP_REST_Request $request Request object. * * @return bool|\WP_Error $prepared_item */ public function prepare_item_for_database( $request ) { $data = json_decode( $request->get_body(), true ); $validate = booktics_validate( $data, array( 'start_time' => array( 'required', ), 'end_time' => array( 'required', ), 'date' => array( 'required' ), 'service_id' => array( 'required', ), 'team_member_id' => array( 'required', ), ) ); if ( is_wp_error( $validate ) ) { return $validate; } $data = $this->get_sanitized_data( $data ); return $data; } private function get_sanitized_data( $data ) { $data['start_time'] = ! empty( $data['start_time'] ) ? sanitize_text_field( $data['start_time'] ) : ''; $data['end_time'] = ! empty( $data['end_time'] ) ? sanitize_text_field( $data['end_time'] ) : ''; $data['date'] = ! empty( $data['date'] ) ? sanitize_text_field( $data['date'] ) : ''; $data['service_id'] = ! empty( $data['service_id'] ) ? sanitize_text_field( $data['service_id'] ) : ''; $data['team_member_id'] = ! empty( $data['team_member_id'] ) ? sanitize_text_field( $data['team_member_id'] ) : ''; $data['customer_id'] = ! empty( $data['customer_id'] ) ? sanitize_text_field( $data['customer_id'] ) : ''; $data['total_attendees'] = ! empty( $data['total_attendees'] ) ? sanitize_text_field( $data['total_attendees'] ) : ''; $data['status'] = isset( $data['status'] ) ? $data['status'] : 'pending'; return $data; } /** * Check if a given request has access to get time slots. * * @param $request * * @return boolean */ public function get_time_slots_permissions_check( $request ) { return true; } /** * Fetch appointment time slots * * @param WP_REST_Request $request Request object. * * @return array|\WP_HTTP_Response * @throws Exception If there's an error getting time slots. */ public function get_time_slots( $request ) { $service_id = ! empty( $request['service_id'] ) ? absint( $request['service_id'] ) : 0; $additional_duration_id = ! empty( $request['additional_duration_id'] ) ? absint( $request['additional_duration_id'] ) : 0; $team_member_id = ! empty( $request['team_member_id'] ) ? absint( $request['team_member_id'] ) : 0; $start_date = ! empty( $request['start_date'] ) ? sanitize_text_field( $request['start_date'] ) : ''; $end_date = ! empty( $request['end_date'] ) ? sanitize_text_field( $request['end_date'] ) : ''; $total_attendees = isset( $request['total_attendees'] ) ? absint( $request['total_attendees'] ) : 1; $db = new Booktics_Database(); $service_schedules = $db->table( booktics_get_table_name( 'booktics_schedules' ) ) ->where( 'service_id', '=', $service_id ) ->where_group( function ( $q ) use ( $start_date, $end_date ) { $q->where( 'custom_date', '=', '0' ) ->or_where_group( function ( $q2 ) use ( $start_date, $end_date ) { $q2->where( 'custom_date', '>', $start_date ) ->where( 'custom_date', '<', $end_date ); } ); } ) ->to_array(); $service_schedules = new Service_Schedule( $service_schedules, $service_id ); $team_member_schedules = $db->table( booktics_get_table_name( 'booktics_schedules' ) ) ->where( 'team_member_id', '=', $team_member_id ) ->where_group( function ( $q ) use ( $start_date, $end_date ) { $q->where( 'custom_date', '=', '0' ) ->or_where_group( function ( $q2 ) use ( $start_date, $end_date ) { $q2->where( 'custom_date', '>', $start_date ) ->where( 'custom_date', '<', $end_date ); } ); } ) ->to_array(); $team_member_schedules = new Team_Member_Schedule( $team_member_schedules, $team_member_id ); $default_schedules = $db->table( booktics_get_table_name( 'booktics_schedules' ) ) ->where( 'service_id', '=', 0 ) ->where( 'team_member_id', '=', 0 ) ->where_group( function ( $q ) use ( $start_date, $end_date ) { $q->where( 'custom_date', '=', '0' ) ->or_where_group( function ( $q2 ) use ( $start_date, $end_date ) { $q2->where( 'custom_date', '>', $start_date ) ->where( 'custom_date', '<', $end_date ); } ); } ) ->to_array(); $default_schedules = new Default_Schedule( $default_schedules ); $service = Service_Model::find( $service_id ); $service_duration = $this->get_service_duration_from_additional_durations( $service->additional_durations, $additional_duration_id ); $service_duration = apply_filters( 'booktics_service_duration_for_appointment_time_slots', $service_duration, $request ); $slot_length = $service->interval_time < 1 ? booktics_get_option( 'interval_time', 30 ) : $service->interval_time; // Get buffer times from service $before_buffer_time = $service->before_buffer_time ? intval( $service->before_buffer_time ) : 0; $after_buffer_time = $service->after_buffer_time ? intval( $service->after_buffer_time ) : 0; $options = array( 'default_schedule' => $default_schedules, 'service_duration' => $service_duration, 'slot_length' => $slot_length, 'before_buffer_time' => $before_buffer_time, 'after_buffer_time' => $after_buffer_time, 'service_id' => $service_id, 'total_attendees' => $total_attendees, ); // Generate time slots for the given date range by interval time $slots = ( new Time_Slot_Manager( $service_schedules, $team_member_schedules, $options ) ) ->generate_slots_for_range( $start_date, $end_date ); $response = array( 'service_id' => $service_id, 'team_member_id' => $team_member_id, 'additional_duration_id' => $additional_duration_id, 'start_date' => $start_date, 'end_date' => $end_date, 'slots' => $slots, ); return $this->response( $response, __( 'Successfully fetched time slots', 'booktics' ) ); } /** * Get service duration from additional durations using additional_duration_id * If additional_duration_id is not found, return default duration (30 minutes) * * @param array $additional_durations Array of additional durations. * @param int $additional_duration_id ID of additional duration. * * @return int Service duration in minutes. */ private function get_service_duration_from_additional_durations( $additional_durations, $additional_duration_id ) { $default_duration = 30; if ( empty( $additional_durations ) ) { return $default_duration; // Return default duration if additional durations are empty } foreach ( $additional_durations as $additional_duration ) { if ( $additional_duration['id'] === $additional_duration_id ) { return $this->get_converted_duration( $additional_duration['duration_time'], $additional_duration['duration_unit'] ); } } return $default_duration; } /** * Convert duration to minutes * * @param int $duration Duration in minutes or hours. * @param string $unit Unit of duration (min or hour). * * @return int Duration in minutes. */ private function get_converted_duration($duration, $unit) { if ( $unit === 'min' ) { return $duration; } elseif ( $unit === 'hour' ) { return $duration * 60; } } }