PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.24
Booktics – Appointment Booking Calendar for Service Businesses v1.0.24
1.0.29 1.0.28 1.0.27 1.0.26 1.0.25 1.0.24 1.0.23 1.0.22 1.0.21 1.0.20 1.0.19 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.2 1.0.3 All 31 releases
booktics / core / appointment / controllers / appointment-controller.php

appointment-controller.php in Booktics – Appointment Booking Calendar for Service Businesses 1.0.24, at core/appointment/controllers/appointment-controller.php

1,139 lines 42.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Booktics\Appointment\Controllers;
4
5 use Booktics\Abstracts\Base_Rest_Controller;
6 use Booktics\Abstracts\Booktics_Database;
7 use Booktics\Appointment\Schedule\Default_Schedule;
8 use Booktics\Appointment\Schedule\Service_Schedule;
9 use Booktics\Appointment\Schedule\Team_Member_Schedule;
10 use Booktics\Appointment\Schedule\Time_Slot_Manager;
11 use Booktics\Appointment\Services\Appointment_Service;
12 use Booktics\Appointment\Services\Appointment_Validator;
13 use Booktics\Models\Appointment_Model;
14 use Booktics\Models\Service_Model;
15 use Booktics\Models\Team_Member_Model;
16 use Booktics\Models\Guest_Model;
17 use Exception;
18 use WP_Error;
19 use WP_HTTP_Response;
20 use WP_REST_Request;
21 use WP_REST_Response;
22 use WP_REST_Server;
23
24 /**
25 * Appointment controller
26 *
27 * @package Booktics/Appointment
28 */
29 class Appointment_Controller extends Base_Rest_Controller {
30 /**
31 * Endpoint namespace
32 *
33 * @var string
34 */
35 protected $namespace = 'booktics/v1';
36
37 /**
38 * Route name
39 *
40 * @var string
41 */
42 protected $base = 'appointments';
43
44 /**
45 * Register routes
46 *
47 * @return void
48 */
49 public function register_routes(): void {
50 register_rest_route(
51 $this->namespace,
52 $this->base,
53 array(
54 array(
55 'methods' => WP_REST_Server::CREATABLE,
56 'callback' => array( $this, 'create_item' ),
57 'permission_callback' => array(
58 $this,
59 'create_item_permissions_check',
60 ),
61 ),
62 )
63 );
64
65 register_rest_route(
66 $this->namespace,
67 $this->base,
68 array(
69 array(
70 'methods' => WP_REST_Server::READABLE,
71 'callback' => array( $this, 'get_items' ),
72 'permission_callback' => array(
73 $this,
74 'get_items_permissions_check',
75 ),
76 ),
77 )
78 );
79
80 register_rest_route(
81 $this->namespace,
82 $this->base,
83 array(
84 array(
85 'methods' => WP_REST_Server::DELETABLE,
86 'callback' => array( $this, 'delete_items' ),
87 'permission_callback' => array(
88 $this,
89 'delete_items_permissions_check',
90 ),
91 ),
92 )
93 );
94
95 register_rest_route(
96 $this->namespace,
97 $this->base . '/(?P<id>[\d]+)',
98 array(
99 array(
100 'methods' => WP_REST_Server::READABLE,
101 'callback' => array( $this, 'get_item' ),
102 'permission_callback' => array(
103 $this,
104 'get_item_permissions_check',
105 ),
106 ),
107 )
108 );
109
110 register_rest_route(
111 $this->namespace,
112 $this->base . '/(?P<id>[\d]+)',
113 array(
114 array(
115 'methods' => WP_REST_Server::EDITABLE,
116 'callback' => array( $this, 'update_item' ),
117 'permission_callback' => array(
118 $this,
119 'update_item_permissions_check',
120 ),
121 ),
122 )
123 );
124
125 register_rest_route(
126 $this->namespace,
127 $this->base . '/(?P<id>[\d]+)',
128 array(
129 array(
130 'methods' => WP_REST_Server::DELETABLE,
131 'callback' => array( $this, 'delete_item' ),
132 'permission_callback' => array(
133 $this,
134 'delete_item_permissions_check',
135 ),
136 ),
137 )
138 );
139
140 register_rest_route(
141 $this->namespace,
142 $this->base . '/(?P<id>[\d]+)/reschedule',
143 array(
144 array(
145 'methods' => WP_REST_Server::EDITABLE,
146 'callback' => array( $this, 'reschedule_item' ),
147 'permission_callback' => array(
148 $this,
149 'reschedule_item_permissions_check',
150 ),
151 ),
152 )
153 );
154
155 register_rest_route(
156 $this->namespace,
157 $this->base . '/(?P<id>[\d]+)/cancel',
158 array(
159 array(
160 'methods' => WP_REST_Server::EDITABLE,
161 'callback' => array( $this, 'cancel_item' ),
162 'permission_callback' => array(
163 $this,
164 'cancel_item_permissions_check',
165 ),
166 ),
167 )
168 );
169
170 register_rest_route(
171 $this->namespace,
172 $this->base . '/time-slots',
173 array(
174 array(
175 'methods' => WP_REST_Server::READABLE,
176 'callback' => array( $this, 'get_time_slots' ),
177 'permission_callback' => array(
178 $this,
179 'get_time_slots_permissions_check',
180 ),
181 ),
182 )
183 );
184
185 register_rest_route(
186 $this->namespace,
187 $this->base . '/calendar',
188 array(
189 array(
190 'methods' => WP_REST_Server::READABLE,
191 'callback' => array(
192 $this,
193 'get_all_calendar_items',
194 ),
195 'permission_callback' => array(
196 $this,
197 'get_items_permissions_check',
198 ),
199 'args' => array(
200 'month' => array(
201 'description' => __( 'Month in YYYY-MM format', 'booktics' ),
202 'type' => 'string',
203 'validate_callback' => function ( $param ) {
204 return preg_match( '/^\d{4}-\d{2}$/', $param );
205 },
206 ),
207 ),
208 ),
209 )
210 );
211 }
212
213 /**
214 * Get a collection of items.
215 *
216 * @param WP_REST_Request $request Full data about the request.
217 *
218 * @return WP_HTTP_Response
219 */
220 public function get_items( $request ) {
221 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 10;
222 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
223 $date_from = ! empty( $request['date_from'] ) ? sanitize_text_field( $request['date_from'] ) : null;
224
225 $service_id = ! empty( $request['service_id'] ) ? intval( $request['service_id'] ) : null;
226 $customer_id = ! empty( $request['customer_id'] ) ? intval( $request['customer_id'] ) : null;
227 $payment_status = ! empty( $request['payment_status'] ) ? sanitize_text_field( $request['payment_status'] ) : null;
228 $team_member_id = ! empty( $request['team_member_id'] ) ? intval( $request['team_member_id'] ) : null;
229 $status = ! empty( $request['status'] ) ? sanitize_text_field( $request['status'] ) : null;
230
231 $search = ! empty( $request['search'] ) ? sanitize_text_field( $request['search'] ) : null;
232 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering appointments by meta fields
233 $args = array( 'meta_query' => array() );
234
235 if ( $search ) {
236 $service_ids = array();
237 $customer_ids = array();
238 $team_member_ids = array();
239 // Search Services
240 $services = Service_Model::all(
241 array(
242 's' => $search,
243 )
244 );
245 $service_ids = array_map(
246 function ( $service ) {
247 return $service->id;
248 }, $services
249 );
250
251 // Search Customers
252 $customer_query = new \WP_User_Query(
253 array(
254 'search' => '*' . esc_attr( $search ) . '*',
255 'search_columns' => array( 'display_name', 'user_email' ),
256 'role' => 'booktics_customer',
257 'fields' => 'ID',
258 )
259 );
260 $customer_ids = $customer_query->get_results();
261
262 // Search Team Members
263 $team_member_query = new \WP_User_Query(
264 array(
265 'search' => '*' . esc_attr( $search ) . '*',
266 'search_columns' => array( 'display_name', 'user_email' ),
267 'role' => 'booktics_team_member',
268 'fields' => 'ID',
269 )
270 );
271 $team_member_ids = $team_member_query->get_results();
272
273 if ( $service_ids || $customer_ids || $team_member_ids ) {
274 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering users by meta fields
275 $args['meta_query'] = array( 'relation' => 'OR' );
276 if ( $service_ids ) {
277 $args['meta_query'][] = array(
278 'key' => 'service_id',
279 'value' => $service_ids,
280 'compare' => 'IN',
281 );
282 }
283 if ( $customer_ids ) {
284 $args['meta_query'][] = array(
285 'key' => 'customer_id',
286 'value' => $customer_ids,
287 'compare' => 'IN',
288 );
289 }
290 if ( $team_member_ids ) {
291 $args['meta_query'][] = array(
292 'key' => 'team_member_id',
293 'value' => $team_member_ids,
294 'compare' => 'IN',
295 );
296 }
297 } else {
298 return $this->response(
299 array(
300 'items' => array(),
301 'total' => 0,
302 'per_page' => $per_page,
303 'current_page' => $paged,
304 'last_page' => 0,
305 ), __( 'No appointments found', 'booktics' )
306 );
307 }
308 }
309
310 if ( $date_from ) {
311 $args['meta_query'][] = array(
312 'key' => 'date',
313 'value' => $date_from,
314 'compare' => '>=',
315 'type' => 'DATE',
316 );
317 }
318
319 if ( $service_id ) {
320 $args['meta_query'][] = array(
321 'key' => 'service_id',
322 'value' => $service_id,
323 'compare' => '=',
324 );
325 }
326
327 if ( $customer_id ) {
328 $args['meta_query'][] = array(
329 'key' => 'customer_id',
330 'value' => $customer_id,
331 'compare' => '=',
332 );
333 }
334
335 if ( $payment_status ) {
336 $args['meta_query'][] = array(
337 'key' => 'payment_status',
338 'value' => $payment_status,
339 'compare' => '=',
340 );
341 }
342
343 if ( $team_member_id ) {
344 $args['meta_query'][] = array(
345 'key' => 'team_member_id',
346 'value' => $team_member_id,
347 'compare' => '=',
348 );
349 }
350
351 // Adds filtering by appointment status
352 if ( $status ) {
353 $args['post_status'] = is_array( $status ) ? $status : array( $status );
354 }
355
356 $args = apply_filters( 'booktics_appointment_args', $args );
357
358 // Scope to customer's own appointments when not admin/team-member
359 if ( ! booktics_is_admin_or_team_member() && is_user_logged_in() ) {
360 $db = new Booktics_Database();
361 $guest = ( new Guest_Model( $db ) )->find( array( 'wp_user_id' => get_current_user_id() ) );
362 if ( ! $guest ) {
363 return $this->response(
364 array(
365 'items' => array(),
366 'total' => 0,
367 'per_page' => $per_page,
368 'current_page' => $paged,
369 'last_page' => 0,
370 ), __( 'No appointments found', 'booktics' )
371 );
372 }
373 // Add customer_id filter to meta_query
374 $args['meta_query'][] = array(
375 'key' => 'customer_id',
376 'value' => $guest->id,
377 'compare' => '=',
378 );
379 }
380
381 $data = Appointment_Model::paginate( $per_page, $paged, $args );
382
383 $data = apply_filters( 'booktics_appointment_data', $data );
384
385 // Mask team member email for customers (non-admin/team-member)
386 if ( ! booktics_is_admin_or_team_member() && is_user_logged_in() && ! empty( $data['items'] ) ) {
387 foreach ( $data['items'] as &$appointment ) {
388 if ( isset( $appointment->team_member_id ) ) {
389 $team_member = new Team_Member_Model( $appointment->team_member_id );
390 $team_member_data = $team_member->find( $appointment->team_member_id );
391 if ( $team_member_data ) {
392 // Remove user_email from team data to protect privacy
393 $team_member_data->user_email = '';
394 }
395 }
396 }
397 }
398
399 if ( ! $data ) {
400 return $this->error( __( 'No appointment found', 'booktics' ), 404 );
401 }
402
403 return $this->response( $data );
404 }
405
406 /**
407 * Get one item from the collection.
408 *
409 * @param WP_REST_Request $request Full data about the request.
410 *
411 * @return WP_Error|WP_REST_Response
412 */
413 public function get_item( $request ) {
414 $id = intval( $request['id'] );
415
416 $appointment = Appointment_Model::find( $id );
417
418 if ( ! $appointment ) {
419 return $this->error( __( 'Appointment not found', 'booktics' ), 404 );
420 }
421
422 // Mask team member email for customers (non-admin/team-member)
423 if ( ! booktics_is_admin_or_team_member() && is_user_logged_in() ) {
424 $appointment_array = is_array( $appointment ) ? $appointment : ( is_object( $appointment ) ? get_object_vars( $appointment ) : array() );
425 if ( isset( $appointment_array['team_member_id'] ) ) {
426 $team_member = new Team_Member_Model( $appointment_array['team_member_id'] );
427 $team_member_data = $team_member->find( $appointment_array['team_member_id'] );
428 if ( $team_member_data && isset( $team_member_data->user_email ) ) {
429 // We can't easily unset from the object, but we can return a modified response
430 }
431 }
432 }
433
434 return $this->response( $appointment );
435 }
436
437 /**
438 * Get all calendar items within a date range
439 *
440 * @param WP_REST_Request $request Full data about the request.
441 *
442 * @return WP_Error|WP_REST_Response
443 */
444 public function get_all_calendar_items( $request ) {
445 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 500;
446 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
447
448 // Check if custom date range is provided
449 if ( ! empty( $request['start_date'] ) && ! empty( $request['end_date'] ) ) {
450 $adjusted_start_date = sanitize_text_field( $request['start_date'] );
451 $adjusted_end_date = sanitize_text_field( $request['end_date'] );
452 $month = gmdate( 'Y-m', strtotime( $adjusted_start_date ) );
453 } else {
454 // Fall back to month-based date range with week adjustment
455 $month = ! empty( $request['month'] ) ? sanitize_text_field( $request['month'] ) : gmdate( 'Y-m' );
456 $start_date = gmdate( 'Y-m-01', strtotime( $month ) );
457 $end_date = gmdate( 'Y-m-t', strtotime( $month ) );
458
459 // Get the first day of the week (Sunday = 0, Monday = 1, etc.)
460 $first_day_of_week = get_option( 'start_of_week', 1 );
461
462 // Adjust start date to include days from previous month to complete the first week
463 $start_date_obj = new \DateTime( $start_date );
464 $start_week_day = (int) $start_date_obj->format( 'w' ); // 0 (Sun) to 6 (Sat)
465 $days_to_subtract = ( $start_week_day - $first_day_of_week + 7 ) % 7;
466 $start_date_obj->modify( "-$days_to_subtract days" );
467 $adjusted_start_date = $start_date_obj->format( 'Y-m-d' );
468
469 // Adjust end date to include days from next month to complete the last week
470 $end_date_obj = new \DateTime( $end_date );
471 $end_week_day = (int) $end_date_obj->format( 'w' ); // 0 (Sun) to 6 (Sat)
472 $days_to_add = ( 6 - $end_week_day + $first_day_of_week ) % 7;
473 $end_date_obj->modify( "+$days_to_add days" );
474 $adjusted_end_date = $end_date_obj->format( 'Y-m-d' );
475 }
476
477 // Get all appointments within the date range using meta_query
478 $args = array(
479 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering appointments by meta fields
480 'meta_query' => array(
481 'relation' => 'AND',
482 array(
483 'key' => 'date',
484 'value' => array(
485 $adjusted_start_date,
486 $adjusted_end_date,
487 ),
488 'compare' => 'BETWEEN',
489 'type' => 'DATE',
490 ),
491 ),
492 'posts_per_page' => - 1, // Get all appointments in the range
493 'orderby' => 'meta_value',
494 'meta_key' => 'date', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Required for ordering by date meta field
495 'order' => 'ASC',
496 );
497
498 $service_id = ! empty( $request['service_id'] ) ? intval( $request['service_id'] ) : null;
499 $customer_id = ! empty( $request['customer_id'] ) ? intval( $request['customer_id'] ) : null;
500 $payment_status = ! empty( $request['payment_status'] ) ? sanitize_text_field( $request['payment_status'] ) : null;
501 $team_member_id = ! empty( $request['team_member_id'] ) ? intval( $request['team_member_id'] ) : null;
502 $status = ! empty( $request['status'] ) ? sanitize_text_field( $request['status'] ) : null;
503
504 if ( $service_id ) {
505 $args['meta_query'][] = array(
506 'key' => 'service_id',
507 'value' => $service_id,
508 'compare' => '=',
509 );
510 }
511
512 if ( $customer_id ) {
513 $args['meta_query'][] = array(
514 'key' => 'customer_id',
515 'value' => $customer_id,
516 'compare' => '=',
517 );
518 }
519
520 if ( $payment_status ) {
521 $args['meta_query'][] = array(
522 'key' => 'payment_status',
523 'value' => $payment_status,
524 'compare' => '=',
525 );
526 }
527
528 if ( $team_member_id ) {
529 $args['meta_query'][] = array(
530 'key' => 'team_member_id',
531 'value' => $team_member_id,
532 'compare' => '=',
533 );
534 }
535
536 if ( $status ) {
537 $args['meta_query'][] = array(
538 'key' => 'status',
539 'value' => $status,
540 'compare' => '=',
541 );
542 }
543
544 $args = apply_filters( 'booktics_appointment_args', $args );
545
546 $appointments = Appointment_Model::paginate( $per_page, $paged, $args );
547
548 // Format the response
549 $formatted_appointments = array();
550
551 if ( ! empty( $appointments ) ) {
552 foreach ( $appointments['items'] as $appointment ) {
553 if ( ! empty( $appointment ) ) {
554 $appointment->source = 'booktics';
555 }
556
557 $formatted_appointments[] = $appointment;
558 }
559 }
560
561 // Allow other developers to add/modify appointments
562 $formatted_appointments = apply_filters(
563 'booktics_appointments_for_calendar_view',
564 $formatted_appointments,
565 array(
566 'start_date' => $adjusted_start_date,
567 'end_date' => $adjusted_end_date,
568 )
569 );
570
571 $response = array(
572 'month' => gmdate( 'Y-m', strtotime( $month ) ),
573 'date_range' => array(
574 'start' => $adjusted_start_date,
575 'end' => $adjusted_end_date,
576 ),
577 'appointments' => $formatted_appointments,
578 );
579
580 return $this->response( $response, __( 'Calendar appointments retrieved successfully', 'booktics' ) );
581 }
582
583 /**
584 * Check if a given request has access to get a specific item.
585 *
586 * @param WP_REST_Request $request Full data about the request.
587 *
588 * @return WP_Error|boolean
589 */
590 public function get_item_permissions_check( $request ) {
591 if ( booktics_is_admin_or_team_member() ) {
592 return true;
593 }
594
595 if ( is_user_logged_in() ) {
596 $appointment_id = intval( $request['id'] );
597 $appointment = Appointment_Model::find( $appointment_id );
598
599 if ( $appointment ) {
600 $db = new Booktics_Database();
601 $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $appointment->customer_id ) );
602
603 if ( $customer && intval( $customer->wp_user_id ) === get_current_user_id() ) {
604 return true;
605 }
606 }
607 }
608
609 return new WP_Error(
610 'booktics_forbidden',
611 __( 'You are not allowed to view this appointment.', 'booktics' ),
612 array( 'status' => 403 )
613 );
614 }
615
616 /**
617 * Check if a given request has access to get items.
618 *
619 * @param WP_REST_Request $request Full data about the request.
620 *
621 * @return WP_Error|boolean
622 */
623 public function get_items_permissions_check( $request ) {
624 if ( booktics_is_admin_or_team_member() ) {
625 return true;
626 }
627
628 if ( is_user_logged_in() ) {
629 return true;
630 }
631
632 return new WP_Error(
633 'booktics_forbidden',
634 __( 'You are not allowed to view appointments.', 'booktics' ),
635 array( 'status' => 403 )
636 );
637 }
638
639 /**
640 * Create one item from the collection.
641 *
642 * @param WP_REST_Request $request Full data about the request.
643 *
644 * @return WP_Error|WP_REST_Response
645 * @throws Exception
646 */
647 public function create_item( $request ) {
648 $data = json_decode( $request->get_body(), true );
649 $error = Appointment_Validator::instance()->validate_create( $data );
650 if ( $error ) {
651 return $this->error( $error->get_error_message() );
652 }
653 $appointment = Appointment_Service::instance()->create( $data );
654 if ( is_wp_error( $appointment ) ) {
655 return $this->error( $appointment->get_error_message() );
656 }
657 do_action( 'booktics_after_appointment_created', $appointment );
658
659 return $this->response( $appointment, __( 'Successfully created appointment', 'booktics' ) );
660 }
661
662 /**
663 * Check if a given request has access to create items.
664 *
665 * @param WP_REST_Request $request Full data about the request.
666 *
667 * @return WP_Error|boolean
668 */
669 public function create_item_permissions_check( $request ) {
670 return current_user_can( 'manage_options' );
671 }
672
673 /**
674 * Update one item from the collection.
675 *
676 * @param WP_REST_Request $request Full data about the request.
677 *
678 * @return WP_REST_Response
679 */
680 public function update_item( $request ) {
681 $id = intval( $request['id'] );
682 $data = json_decode( $request->get_body(), true );
683 $error = Appointment_Validator::instance()->validate_update( $data );
684 if ( $error ) {
685 return $this->error( $error->get_error_message() );
686 }
687 $updated_appointment = Appointment_Service::instance()->update( $id, $data );
688 if ( is_wp_error( $updated_appointment ) ) {
689 return $this->error( $updated_appointment->get_error_message() );
690 }
691 do_action( 'booktics_after_appointment_updated', $updated_appointment );
692
693 return $this->response( $updated_appointment, __( 'Appointment updated successfully', 'booktics' ) );
694 }
695
696 /**
697 * Check if a given request has access to update a specific item.
698 *
699 * @param WP_REST_Request $request Full data about the request.
700 *
701 * @return WP_Error|boolean
702 */
703 public function update_item_permissions_check( $request ) {
704 return current_user_can( 'manage_options' );
705 }
706
707 /**
708 * Delete one item from the collection.
709 *
710 * @param WP_REST_Request $request Full data about the request.
711 *
712 * @return \WP_Error|\WP_HTTP_Response
713 */
714 public function delete_item( $request ) {
715 $id = intval( $request['id'] );
716 $result = Appointment_Service::instance()->delete( $id );
717 if ( is_wp_error( $result ) ) {
718 return $this->error( $result->get_error_message() );
719 }
720 do_action( 'booktics_after_appointment_deleted', $result );
721
722 return $this->response( $result, __( 'Successfully deleted appointment', 'booktics' ) );
723 }
724
725 /**
726 * Check if a given request has access to delete a specific item.
727 *
728 * @param WP_REST_Request $request Full data about the request.
729 *
730 * @return WP_Error|boolean
731 */
732 public function delete_item_permissions_check( $request ) {
733 return current_user_can( 'manage_options' );
734 }
735
736 /**
737 * Delete a collection of items
738 *
739 * @param WP_Rest_Request $request User request
740 */
741 public function delete_items( $request ) {
742 $ids = $request['ids'];
743 $result = Appointment_Service::instance()->delete_many( $ids );
744 if ( is_wp_error( $result ) ) {
745 return $this->error( $result->get_error_message() );
746 }
747
748 return $this->response( $result['data'], $result['message'] );
749 }
750
751 /**
752 * Check if a given request has access to delete a collection of items.
753 *
754 * @param WP_REST_Request $request Full data about the request.
755 *
756 * @return WP_Error|boolean
757 */
758 public function delete_items_permissions_check( $request ) {
759 return current_user_can( 'manage_options' );
760 }
761
762 /**
763 * Reschedule an appointment
764 *
765 * @param $request Full data about the request.
766 *
767 * @throws Exception
768 */
769 public function reschedule_item( $request ) {
770 $id = intval( $request['id'] );
771 $data = json_decode( $request->get_body(), true );
772 $error = Appointment_Validator::instance()->validate_reschedule( $data, $id );
773 if ( $error ) {
774 return $this->error( $error->get_error_message() );
775 }
776 $result = Appointment_Service::instance()->reschedule( $id, $data );
777 if ( is_wp_error( $result ) ) {
778 return $this->error( $result->get_error_message() );
779 }
780
781 $this->send_reschedule_email( $result );
782
783 do_action( 'booktics_after_appointment_rescheduled', $result );
784
785 return $this->response( $result, __( 'Successfully rescheduled appointment', 'booktics' ) );
786 }
787
788 /**
789 * Check if a given request has access to reschedule a specific item.
790 *
791 * @param $request Full data about the request.
792 */
793 public function reschedule_item_permissions_check( $request ) {
794 if ( booktics_is_admin_or_team_member() ) {
795 return true;
796 }
797
798 if ( is_user_logged_in() ) {
799 $appointment_id = intval( $request['id'] );
800 $appointment = Appointment_Model::find( $appointment_id );
801
802 if ( $appointment ) {
803 $db = new Booktics_Database();
804 $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $appointment->customer_id ) );
805
806 if ( $customer && intval( $customer->wp_user_id ) === get_current_user_id() ) {
807 return true;
808 }
809 }
810 }
811
812 return new WP_Error(
813 'booktics_forbidden',
814 __( 'You are not allowed to reschedule this appointment.', 'booktics' ),
815 array( 'status' => 403 )
816 );
817 }
818
819 /**
820 * Send reschedule email to customer
821 *
822 * @param Appointment_Model $appointment Appointment model.
823 */
824 private function send_reschedule_email( $appointment ) {
825 $db = new Booktics_Database();
826 $customer = ( new Guest_Model( $db ) )->find( [ 'id' => $appointment->customer_id ] );
827 $team_member = new Team_Member_Model( $appointment->team_member_id );
828
829 $appointment_reschedule_email_data = [
830 'date' => $appointment->date ?? '',
831 'start_time' => $appointment->start_time ?? '',
832 'end_time' => $appointment->end_time ?? '',
833 'customer_name' => $customer->name ?? '',
834 'user_email' => $customer->email ?? '',
835 'appointment_service' => $appointment->title ?? '',
836 'appointment_team_member' => $team_member->display_name ?? '',
837 'appointment_team_member_email' => $team_member->user_email ?? '',
838 ];
839
840 $appointment_reschedule_email_data = apply_filters( 'booktics_appointment_reschedule_email_data', $appointment_reschedule_email_data );
841
842 do_action( 'booktics_gln_hook', 'appointment_rescheduled', $appointment_reschedule_email_data );
843 }
844
845 /**
846 * Cancel an appointment
847 *
848 * @param WP_REST_Request $request Full data about the request.
849 *
850 * @throws Exception
851 */
852 public function cancel_item( $request ) {
853 $id = intval( $request['id'] );
854 $data = json_decode( $request->get_body(), true );
855 $cancel_note = ! empty( $data['cancel_note'] ) ? sanitize_text_field( $data['cancel_note'] ) : '';
856
857 $error = Appointment_Validator::instance()->validate_cancel( $id );
858
859 if ( $error ) {
860 return $this->error( $error->get_error_message() );
861 }
862
863 $result = Appointment_Service::instance()->cancel( $id, [ 'cancel_note' => $cancel_note ] );
864
865 if ( is_wp_error( $result ) ) {
866 return $this->error( $result->get_error_message() );
867 }
868
869 $this->send_cancel_email( $result );
870
871 // Note: booktics_after_appointment_cancelled is fired inside
872 // Appointment_Service::cancel(), the single choke point every cancel
873 // path routes through, so it is intentionally not re-fired here.
874
875 return $this->response( $result, __( 'Successfully cancelled appointment', 'booktics' ) );
876 }
877
878 /**
879 * Send cancel email to customer
880 *
881 * @param Appointment_Model $appointment Appointment model.
882 */
883 private function send_cancel_email( $appointment ) {
884 $db = new Booktics_Database();
885 $customer = ( new Guest_Model( $db ) )->find( [ 'id' => $appointment->customer_id ] );
886 $team_member = new Team_Member_Model( $appointment->team_member_id );
887
888 $appointment_cancel_email_data = [
889 'date' => $appointment->date ?? '',
890 'start_time' => $appointment->start_time ?? '',
891 'end_time' => $appointment->end_time ?? '',
892 'customer_name' => $customer->name ?? '',
893 'user_email' => $customer->email ?? '',
894 'appointment_service' => $appointment->title ?? '',
895 'appointment_team_member' => $team_member->display_name ?? '',
896 'appointment_team_member_email' => $team_member->user_email ?? '',
897 ];
898
899 $appointment_cancel_email_data = apply_filters( 'booktics_appointment_cancel_email_data', $appointment_cancel_email_data );
900
901 do_action( 'booktics_gln_hook', 'appointment_cancelled', $appointment_cancel_email_data );
902 }
903
904 /**
905 * Check if a given request has access to cancel a specific item.
906 *
907 * @param WP_REST_Request $request Full data about the request.
908 *
909 * @return WP_Error|boolean
910 */
911 public function cancel_item_permissions_check( $request ) {
912 if ( booktics_is_admin_or_team_member() ) {
913 return true;
914 }
915
916 if ( is_user_logged_in() ) {
917 $appointment_id = intval( $request['id'] );
918 $appointment = Appointment_Model::find( $appointment_id );
919
920 if ( $appointment ) {
921 $db = new Booktics_Database();
922 $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $appointment->customer_id ) );
923
924 if ( $customer && intval( $customer->wp_user_id ) === get_current_user_id() ) {
925 return true;
926 }
927 }
928 }
929
930 return new WP_Error(
931 'booktics_forbidden',
932 __( 'You are not allowed to cancel this appointment.', 'booktics' ),
933 array( 'status' => 403 )
934 );
935 }
936
937 /**
938 * Prepare the item for create or update operation.
939 *
940 * @param WP_REST_Request $request Request object.
941 *
942 * @return bool|\WP_Error $prepared_item
943 */
944 public function prepare_item_for_database( $request ) {
945 $data = json_decode( $request->get_body(), true );
946 $validate = booktics_validate(
947 $data,
948 array(
949 'start_time' => array(
950 'required',
951 ),
952 'end_time' => array(
953 'required',
954 ),
955 'date' => array( 'required' ),
956 'service_id' => array(
957 'required',
958 ),
959 'team_member_id' => array(
960 'required',
961 ),
962 )
963 );
964
965 if ( is_wp_error( $validate ) ) {
966 return $validate;
967 }
968
969 $data = $this->get_sanitized_data( $data );
970
971 return $data;
972 }
973
974 private function get_sanitized_data( $data ) {
975 $data['start_time'] = ! empty( $data['start_time'] ) ? sanitize_text_field( $data['start_time'] ) : '';
976 $data['end_time'] = ! empty( $data['end_time'] ) ? sanitize_text_field( $data['end_time'] ) : '';
977 $data['date'] = ! empty( $data['date'] ) ? sanitize_text_field( $data['date'] ) : '';
978 $data['service_id'] = ! empty( $data['service_id'] ) ? sanitize_text_field( $data['service_id'] ) : '';
979 $data['team_member_id'] = ! empty( $data['team_member_id'] ) ? sanitize_text_field( $data['team_member_id'] ) : '';
980 $data['customer_id'] = ! empty( $data['customer_id'] ) ? sanitize_text_field( $data['customer_id'] ) : '';
981 $data['total_attendees'] = ! empty( $data['total_attendees'] ) ? sanitize_text_field( $data['total_attendees'] ) : '';
982 $data['status'] = isset( $data['status'] ) ? $data['status'] : 'pending';
983
984 return $data;
985 }
986
987 /**
988 * Check if a given request has access to get time slots.
989 *
990 * @param $request
991 *
992 * @return boolean
993 */
994 public function get_time_slots_permissions_check( $request ) {
995 return true;
996 }
997
998 /**
999 * Fetch appointment time slots
1000 *
1001 * @param WP_REST_Request $request Request object.
1002 *
1003 * @return array|\WP_HTTP_Response
1004 * @throws Exception If there's an error getting time slots.
1005 */
1006 public function get_time_slots( $request ) {
1007 $service_id = ! empty( $request['service_id'] ) ? absint( $request['service_id'] ) : 0;
1008 $additional_duration_id = ! empty( $request['additional_duration_id'] ) ? absint( $request['additional_duration_id'] ) : 0;
1009 $team_member_id = ! empty( $request['team_member_id'] ) ? absint( $request['team_member_id'] ) : 0;
1010 $start_date = ! empty( $request['start_date'] ) ? sanitize_text_field( $request['start_date'] ) : '';
1011 $end_date = ! empty( $request['end_date'] ) ? sanitize_text_field( $request['end_date'] ) : '';
1012 $total_attendees = isset( $request['total_attendees'] ) ? absint( $request['total_attendees'] ) : 1;
1013
1014 $db = new Booktics_Database();
1015
1016 $service_schedules = $db->table( booktics_get_table_name( 'booktics_schedules' ) )
1017 ->where( 'service_id', '=', $service_id )
1018 ->where_group(
1019 function ( $q ) use ( $start_date, $end_date ) {
1020 $q->where( 'custom_date', '=', '0' )
1021 ->or_where_group(
1022 function ( $q2 ) use ( $start_date, $end_date ) {
1023 $q2->where( 'custom_date', '>', $start_date )
1024 ->where( 'custom_date', '<', $end_date );
1025 }
1026 );
1027 }
1028 )
1029 ->to_array();
1030 $service_schedules = new Service_Schedule( $service_schedules, $service_id );
1031
1032 $team_member_schedules = $db->table( booktics_get_table_name( 'booktics_schedules' ) )
1033 ->where( 'team_member_id', '=', $team_member_id )
1034 ->where_group(
1035 function ( $q ) use ( $start_date, $end_date ) {
1036 $q->where( 'custom_date', '=', '0' )
1037 ->or_where_group(
1038 function ( $q2 ) use ( $start_date, $end_date ) {
1039 $q2->where( 'custom_date', '>', $start_date )
1040 ->where( 'custom_date', '<', $end_date );
1041 }
1042 );
1043 }
1044 )
1045 ->to_array();
1046 $team_member_schedules = new Team_Member_Schedule( $team_member_schedules, $team_member_id );
1047
1048 $default_schedules = $db->table( booktics_get_table_name( 'booktics_schedules' ) )
1049 ->where( 'service_id', '=', 0 )
1050 ->where( 'team_member_id', '=', 0 )
1051 ->where_group(
1052 function ( $q ) use ( $start_date, $end_date ) {
1053 $q->where( 'custom_date', '=', '0' )
1054 ->or_where_group(
1055 function ( $q2 ) use ( $start_date, $end_date ) {
1056 $q2->where( 'custom_date', '>', $start_date )
1057 ->where( 'custom_date', '<', $end_date );
1058 }
1059 );
1060 }
1061 )
1062 ->to_array();
1063 $default_schedules = new Default_Schedule( $default_schedules );
1064
1065 $service = Service_Model::find( $service_id );
1066 $service_duration = $this->get_service_duration_from_additional_durations( $service->additional_durations, $additional_duration_id );
1067 $service_duration = apply_filters( 'booktics_service_duration_for_appointment_time_slots', $service_duration, $request );
1068 $slot_length = $service->interval_time < 1 ? booktics_get_option( 'interval_time', 30 ) : $service->interval_time;
1069
1070 // Get buffer times from service
1071 $before_buffer_time = $service->before_buffer_time ? intval( $service->before_buffer_time ) : 0;
1072 $after_buffer_time = $service->after_buffer_time ? intval( $service->after_buffer_time ) : 0;
1073 $options = array(
1074 'default_schedule' => $default_schedules,
1075 'service_duration' => $service_duration,
1076 'slot_length' => $slot_length,
1077 'before_buffer_time' => $before_buffer_time,
1078 'after_buffer_time' => $after_buffer_time,
1079 'service_id' => $service_id,
1080 'total_attendees' => $total_attendees,
1081 );
1082 // Generate time slots for the given date range by interval time
1083 $slots = ( new Time_Slot_Manager( $service_schedules, $team_member_schedules, $options ) )
1084 ->generate_slots_for_range( $start_date, $end_date );
1085
1086
1087 $response = array(
1088 'service_id' => $service_id,
1089 'team_member_id' => $team_member_id,
1090 'additional_duration_id' => $additional_duration_id,
1091 'start_date' => $start_date,
1092 'end_date' => $end_date,
1093 'slots' => $slots,
1094 );
1095
1096 return $this->response( $response, __( 'Successfully fetched time slots', 'booktics' ) );
1097 }
1098
1099 /**
1100 * Get service duration from additional durations using additional_duration_id
1101 * If additional_duration_id is not found, return default duration (30 minutes)
1102 *
1103 * @param array $additional_durations Array of additional durations.
1104 * @param int $additional_duration_id ID of additional duration.
1105 *
1106 * @return int Service duration in minutes.
1107 */
1108 private function get_service_duration_from_additional_durations( $additional_durations, $additional_duration_id ) {
1109 $default_duration = 30;
1110 if ( empty( $additional_durations ) ) {
1111 return $default_duration; // Return default duration if additional durations are empty
1112 }
1113
1114 foreach ( $additional_durations as $additional_duration ) {
1115 if ( $additional_duration['id'] === $additional_duration_id ) {
1116 return $this->get_converted_duration( $additional_duration['duration_time'], $additional_duration['duration_unit'] );
1117 }
1118 }
1119
1120 return $default_duration;
1121 }
1122
1123 /**
1124 * Convert duration to minutes
1125 *
1126 * @param int $duration Duration in minutes or hours.
1127 * @param string $unit Unit of duration (min or hour).
1128 *
1129 * @return int Duration in minutes.
1130 */
1131 private function get_converted_duration($duration, $unit) {
1132 if ( $unit === 'min' ) {
1133 return $duration;
1134 } elseif ( $unit === 'hour' ) {
1135 return $duration * 60;
1136 }
1137 }
1138 }
1139