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; $args = array( 'meta_query' => array() ); if ( $search ) { $service_ids = []; $customer_ids = []; $team_member_ids = []; // Search Services $services = Service_Model::all( [ 's' => $search, ] ); $service_ids = array_map( function ( $service ) { return $service->id; }, $services ); // Search Customers $customer_query = new \WP_User_Query( [ 'search' => '*' . esc_attr( $search ) . '*', 'search_columns' => [ '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( [ 'search' => '*' . esc_attr( $search ) . '*', 'search_columns' => [ '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 ) { $args['meta_query'] = [ 'relation' => 'OR' ]; if ( $service_ids ) { $args['meta_query'][] = [ 'key' => 'service_id', 'value' => $service_ids, 'compare' => 'IN' ]; } if ( $customer_ids ) { $args['meta_query'][] = [ 'key' => 'customer_id', 'value' => $customer_ids, 'compare' => 'IN' ]; } if ( $team_member_ids ) { $args['meta_query'][] = [ 'key' => 'team_member_id', 'value' => $team_member_ids, 'compare' => 'IN' ]; } } else { return $this->response( [ 'items' => [], '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' => '=', ); } if ( $status ) { $args['meta_query'][] = array( 'key' => 'status', 'value' => $status, 'compare' => '=', ); } $data = Appointment_Model::paginate( $per_page, $paged, $args ); 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 ); } 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 = date( '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( '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', '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' => '=', ); } $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 ) { return true; } /** * 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 ) { return true; } /** * 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 = $this->prepare_item_for_database( $request ); if ( is_wp_error( $data ) ) { return $this->error( $data->get_error_message() ); } $appointment = Appointment_Model::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 ); if ( empty( $data ) || ! is_array( $data ) ) { return $this->error( __( 'No data provided for update', 'booktics' ), 400 ); } // Get the appointment to update $appointment = Appointment_Model::find( $id ); if ( ! $appointment ) { return $this->error( __( 'Appointment not found', 'booktics' ), 404 ); } if ( isset( $data ['status'] ) ) { if ( ! in_array( $data ['status'], Appointment_Model::get_accepted_statuses() ) ) { return $this->error( __( 'Invalid status', 'booktics' ), 400 ); } $appointment->status = $data ['status']; } if ( ! empty( $data['customer'] ) ) { $customer = ( new Customer_Model() )->find( $appointment->customer_id ); if ( $customer ) { $updated_customer = $customer->update( $data['customer'] ); unset( $data['customer'] ); } } $updated_appointment = $appointment->update( $data ); if ( isset( $updated_customer ) ) { $updated_appointment->customer = $updated_customer; } if ( is_wp_error( $updated_appointment ) ) { return $this->error( $updated_appointment->get_error_message() ); } do_action( 'booktics_after_appointment_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'] ); $appointment = Appointment_Model::find( $id ); if ( ! $appointment ) { return $this->error( __( 'Appointment not found', 'booktics' ), 404 ); } $deleted = $appointment->delete(); if ( is_wp_error( $deleted ) ) { return $this->error( $deleted->get_error_message() ); } do_action( 'booktics_after_appointment_deleted', $appointment ); return $this->response( $appointment, __( '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']; if ( ! is_array( $ids ) ) { return $this->error( __( 'Ids should be an array of items ids', 'booktics' ) ); } $count = 0; $total = count( $ids ); foreach ( $ids as $id ) { $appointment = Appointment_Model::find( $id ); if ( $appointment ) { $appointment->delete(); ++ $count; } } if ( $count === 0 ) { return $this->error( __( 'No item deleted.', 'booktics' ) ); } $message = sprintf( 'Deleted %d items of %d', $count, $total ); return $this->response( array(), $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 ); $validate = booktics_validate( $data, array( 'start_time' => array( 'required' ), 'end_time' => array( 'required' ), 'date' => array( 'required' ), ) ); if ( is_wp_error( $validate ) ) { return $this->error( $validate->get_error_message() ); } $appointment = Appointment_Model::find( $id ); if ( ! $appointment ) { return $this->error( __( 'Appointment not found', 'booktics' ), 404 ); } $db = new Booktics_Database(); $service_schedules = $db->table( 'booktics_schedules' ) ->where( 'service_id', '=', $appointment->service_id ) ->where( 'custom_date', '=', '0' ) ->to_array(); $service_schedules = new Service_Schedule( $service_schedules, $appointment->service_id ); $team_member_schedules = $db->table( 'booktics_schedules' ) ->where( 'team_member_id', '=', $appointment->team_member_id ) ->where( 'custom_date', '=', '0' ) ->to_array(); $team_member_schedules = new Team_Member_Schedule( $team_member_schedules, $appointment->team_member_id ); $default_schedules = $db->table( 'booktics_schedules' ) ->where( 'service_id', '=', 0 ) ->where( 'team_member_id', '=', 0 ) ->where( 'custom_date', '=', '0' ) ->to_array(); $default_schedules = new Default_Schedule( $default_schedules ); $slots = ( new Time_Slot_Manager( $service_schedules, $team_member_schedules, $default_schedules ) ) ->generate_slots_for_range( $data['date'], $data['date'] ); if ( is_wp_error( $validate ) ) { return $this->error( $validate->get_error_message() ); } $updated = $appointment->update( array( 'start_time' => sanitize_text_field( $data['start_time'] ), 'end_time' => sanitize_text_field( $data['end_time'] ), 'date' => sanitize_text_field( $data['date'] ), ) ); if ( is_wp_error( $updated ) ) { return $this->error( __( 'Appointment reschedule error.', 'booktics' ) ); } $service = Service_Model::find( $appointment->service_id ); $team_member = ( new Team_Member_Model() )->find( $appointment->team_member_id ); $customer = ( new Customer_Model() )->find( $appointment->customer_id ); do_action( 'booktics_after_appointment_rescheduled', $appointment ); do_action( 'global_notification_hook', 'appointment_rescheduled', [ 'appointment_service' => $service->title, 'date' => $appointment->date, 'appointment_team_member' => $team_member->display_name, 'start_time' => $appointment->start_time, 'end_time' => $appointment->end_time, 'user_email' => $customer->user_email, ] ); return $this->response( $appointment, __( '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 ) { return true; } /** * Cancel an appointment * * @param WP_REST_Request $request Full data about the request. * * @throws Exception */ public function cancel_item( $request ) { $id = intval( $request['id'] ); $appointment = Appointment_Model::find( $id ); if ( ! $appointment ) { return $this->error( __( 'Appointment not found', 'booktics' ), 404 ); } $updated = $appointment->update( array( 'status' => 'cancelled' ) ); if ( is_wp_error( $updated ) ) { return $this->error( __( 'Appointment cancelletion error.', 'booktics' ) ); } $service = Service_Model::find( $appointment->service_id ); $team_member = ( new Team_Member_Model() )->find( $appointment->team_member_id ); $customer = ( new Customer_Model() )->find( $appointment->customer_id ); do_action( 'booktics_after_appointment_cancelled', $appointment ); do_action( 'global_notification_hook', 'appointment_cancelled', [ 'appointment_service' => $service->title, 'date' => $appointment->date, 'appointment_team_member' => $team_member->display_name, 'start_time' => $appointment->start_time, 'end_time' => $appointment->end_time, 'user_email' => $customer->user_email, ] ); return $this->response( $appointment, __( 'Successfully cancelled appointment', 'booktics' ) ); } /** * 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 ) { return current_user_can( 'manage_options' ); } /** * 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 = $request['service_id']; $additional_service_id = $request['additional_service_id']; $team_member_id = $request['team_member_id']; $start_date = $request['start_date']; $end_date = $request['end_date']; $db = new Booktics_Database(); $service_schedules = $db->table( '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_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_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_additional_services = ! empty( $service->additional_durations ) ? array_filter( $service->additional_durations, function ( $duration ) use ( $additional_service_id ) { return $duration['id'] === $additional_service_id; } ) : array(); $service_duration = ! empty( $service_additional_services ) ? reset( $service_additional_services )['duration_time'] : 30; $slots = ( new Time_Slot_Manager( $service_schedules, $team_member_schedules, $default_schedules, $service_duration ) ) ->generate_slots_for_range( $start_date, $end_date ); $response = array( 'service_id' => $service_id, 'team_member_id' => $team_member_id, 'additional_service_id' => $additional_service_id, 'start_date' => $start_date, 'end_date' => $end_date, 'slots' => $slots, ); return $this->response( $response, __( 'Successfully fetched time slots', 'booktics' ) ); } }