\WP_REST_Server::READABLE, 'callback' => [ $this, 'index' ], 'permission_callback' => [ $this, 'canBook' ], 'args' => [ 'instructor_id' => [ 'type' => 'integer', 'default' => 0, ], 'offering_id' => [ 'type' => 'integer', 'default' => 0, ], 'duration_minutes' => [ 'type' => 'integer', 'default' => 0, ], 'from' => [ 'type' => 'string', 'default' => '', ], 'to' => [ 'type' => 'string', 'default' => '', ], ], ], [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'create' ], 'permission_callback' => [ $this, 'canManage' ], 'args' => [ 'start_dt' => [ 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ], 'end_dt' => [ 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ], 'duration_minutes' => [ 'type' => 'integer', 'default' => 60, ], 'offering_id' => [ 'type' => 'integer', 'default' => 0, ], 'recurrence' => [ 'type' => 'string', 'default' => 'single', ], 'weeks' => [ 'type' => 'integer', 'default' => 1, ], ], ], ] ); register_rest_route( $route_namespace, '/availability/(?P\d+)', [ [ 'methods' => \WP_REST_Server::DELETABLE, 'callback' => [ $this, 'delete' ], 'permission_callback' => [ $this, 'canManage' ], ], ] ); } public function index( \WP_REST_Request $request ): \WP_REST_Response { $slots = $this->repository->findAvailable( Val::int( $request->get_param( 'instructor_id' ) ), Val::int( $request->get_param( 'offering_id' ) ), Val::int( $request->get_param( 'duration_minutes' ) ), Val::string( $request->get_param( 'from' ) ), Val::string( $request->get_param( 'to' ) ), ); return new \WP_REST_Response( array_map( fn( AvailabilitySlot $s ) => $s->toArray(), $slots ), 200 ); } public function create( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error { // Validation lives in WindowValidator so this endpoint and the admin form // enforce exactly the same rules. $window = $this->validator->validate( get_current_user_id(), Val::string( $request->get_param( 'start_dt' ) ), Val::string( $request->get_param( 'end_dt' ) ), absint( Val::int( $request->get_param( 'duration_minutes' ) ) ), absint( Val::int( $request->get_param( 'offering_id' ) ) ), ); if ( $window instanceof \WP_Error ) { return $window; } $ids = $this->repository->createFromWindow( $window, 'weekly' === $request->get_param( 'recurrence' ), absint( Val::int( $request->get_param( 'weeks' ) ) ) ); // A valid window splits into at least one slot, so nothing written means // every insert failed. if ( [] === $ids ) { return new \WP_Error( 'not_saved', __( 'The availability could not be saved.', 'unsupervised-schedular' ), [ 'status' => 500 ] ); } return new \WP_REST_Response( [ 'ids' => $ids ], 201 ); } public function delete( \WP_REST_Request $request ): \WP_REST_Response|\WP_Error { $id = absint( Val::int( $request->get_param( 'id' ) ) ); $slot = $this->repository->findById( $id ); if ( null === $slot ) { return new \WP_Error( 'not_found', __( 'Slot not found.', 'unsupervised-schedular' ), [ 'status' => 404 ] ); } if ( get_current_user_id() !== $slot->instructorId ) { return new \WP_Error( 'forbidden', __( 'You cannot delete this slot.', 'unsupervised-schedular' ), [ 'status' => 403 ] ); } if ( $slot->isBooked ) { return new \WP_Error( 'slot_booked', __( 'Cannot delete a booked slot.', 'unsupervised-schedular' ), [ 'status' => 409 ] ); } $this->repository->delete( $id ); return new \WP_REST_Response( null, 204 ); } public function canBook(): bool { return is_user_logged_in() && current_user_can( RoleManager::CAP_BOOK_LESSON ); } public function canManage(): bool { return is_user_logged_in() && current_user_can( RoleManager::CAP_MANAGE_AVAILABILITY ); } }