PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.21
Booktics – Appointment Booking Calendar for Service Businesses v1.0.21
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.21, at core/appointment/controllers/appointment-controller.php

1,137 lines 42.5 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 do_action( 'booktics_after_appointment_cancelled', $result );
872
873 return $this->response( $result, __( 'Successfully cancelled appointment', 'booktics' ) );
874 }
875
876 /**
877 * Send cancel email to customer
878 *
879 * @param Appointment_Model $appointment Appointment model.
880 */
881 private function send_cancel_email( $appointment ) {
882 $db = new Booktics_Database();
883 $customer = ( new Guest_Model( $db ) )->find( [ 'id' => $appointment->customer_id ] );
884 $team_member = new Team_Member_Model( $appointment->team_member_id );
885
886 $appointment_cancel_email_data = [
887 'date' => $appointment->date ?? '',
888 'start_time' => $appointment->start_time ?? '',
889 'end_time' => $appointment->end_time ?? '',
890 'customer_name' => $customer->name ?? '',
891 'user_email' => $customer->email ?? '',
892 'appointment_service' => $appointment->title ?? '',
893 'appointment_team_member' => $team_member->display_name ?? '',
894 'appointment_team_member_email' => $team_member->user_email ?? '',
895 ];
896
897 $appointment_cancel_email_data = apply_filters( 'booktics_appointment_cancel_email_data', $appointment_cancel_email_data );
898
899 do_action( 'booktics_gln_hook', 'appointment_cancelled', $appointment_cancel_email_data );
900 }
901
902 /**
903 * Check if a given request has access to cancel a specific item.
904 *
905 * @param WP_REST_Request $request Full data about the request.
906 *
907 * @return WP_Error|boolean
908 */
909 public function cancel_item_permissions_check( $request ) {
910 if ( booktics_is_admin_or_team_member() ) {
911 return true;
912 }
913
914 if ( is_user_logged_in() ) {
915 $appointment_id = intval( $request['id'] );
916 $appointment = Appointment_Model::find( $appointment_id );
917
918 if ( $appointment ) {
919 $db = new Booktics_Database();
920 $customer = ( new Guest_Model( $db ) )->find( array( 'id' => $appointment->customer_id ) );
921
922 if ( $customer && intval( $customer->wp_user_id ) === get_current_user_id() ) {
923 return true;
924 }
925 }
926 }
927
928 return new WP_Error(
929 'booktics_forbidden',
930 __( 'You are not allowed to cancel this appointment.', 'booktics' ),
931 array( 'status' => 403 )
932 );
933 }
934
935 /**
936 * Prepare the item for create or update operation.
937 *
938 * @param WP_REST_Request $request Request object.
939 *
940 * @return bool|\WP_Error $prepared_item
941 */
942 public function prepare_item_for_database( $request ) {
943 $data = json_decode( $request->get_body(), true );
944 $validate = booktics_validate(
945 $data,
946 array(
947 'start_time' => array(
948 'required',
949 ),
950 'end_time' => array(
951 'required',
952 ),
953 'date' => array( 'required' ),
954 'service_id' => array(
955 'required',
956 ),
957 'team_member_id' => array(
958 'required',
959 ),
960 )
961 );
962
963 if ( is_wp_error( $validate ) ) {
964 return $validate;
965 }
966
967 $data = $this->get_sanitized_data( $data );
968
969 return $data;
970 }
971
972 private function get_sanitized_data( $data ) {
973 $data['start_time'] = ! empty( $data['start_time'] ) ? sanitize_text_field( $data['start_time'] ) : '';
974 $data['end_time'] = ! empty( $data['end_time'] ) ? sanitize_text_field( $data['end_time'] ) : '';
975 $data['date'] = ! empty( $data['date'] ) ? sanitize_text_field( $data['date'] ) : '';
976 $data['service_id'] = ! empty( $data['service_id'] ) ? sanitize_text_field( $data['service_id'] ) : '';
977 $data['team_member_id'] = ! empty( $data['team_member_id'] ) ? sanitize_text_field( $data['team_member_id'] ) : '';
978 $data['customer_id'] = ! empty( $data['customer_id'] ) ? sanitize_text_field( $data['customer_id'] ) : '';
979 $data['total_attendees'] = ! empty( $data['total_attendees'] ) ? sanitize_text_field( $data['total_attendees'] ) : '';
980 $data['status'] = isset( $data['status'] ) ? $data['status'] : 'pending';
981
982 return $data;
983 }
984
985 /**
986 * Check if a given request has access to get time slots.
987 *
988 * @param $request
989 *
990 * @return boolean
991 */
992 public function get_time_slots_permissions_check( $request ) {
993 return true;
994 }
995
996 /**
997 * Fetch appointment time slots
998 *
999 * @param WP_REST_Request $request Request object.
1000 *
1001 * @return array|\WP_HTTP_Response
1002 * @throws Exception If there's an error getting time slots.
1003 */
1004 public function get_time_slots( $request ) {
1005 $service_id = ! empty( $request['service_id'] ) ? absint( $request['service_id'] ) : 0;
1006 $additional_duration_id = ! empty( $request['additional_duration_id'] ) ? absint( $request['additional_duration_id'] ) : 0;
1007 $team_member_id = ! empty( $request['team_member_id'] ) ? absint( $request['team_member_id'] ) : 0;
1008 $start_date = ! empty( $request['start_date'] ) ? sanitize_text_field( $request['start_date'] ) : '';
1009 $end_date = ! empty( $request['end_date'] ) ? sanitize_text_field( $request['end_date'] ) : '';
1010 $total_attendees = isset( $request['total_attendees'] ) ? absint( $request['total_attendees'] ) : 1;
1011
1012 $db = new Booktics_Database();
1013
1014 $service_schedules = $db->table( booktics_get_table_name( 'booktics_schedules' ) )
1015 ->where( 'service_id', '=', $service_id )
1016 ->where_group(
1017 function ( $q ) use ( $start_date, $end_date ) {
1018 $q->where( 'custom_date', '=', '0' )
1019 ->or_where_group(
1020 function ( $q2 ) use ( $start_date, $end_date ) {
1021 $q2->where( 'custom_date', '>', $start_date )
1022 ->where( 'custom_date', '<', $end_date );
1023 }
1024 );
1025 }
1026 )
1027 ->to_array();
1028 $service_schedules = new Service_Schedule( $service_schedules, $service_id );
1029
1030 $team_member_schedules = $db->table( booktics_get_table_name( 'booktics_schedules' ) )
1031 ->where( 'team_member_id', '=', $team_member_id )
1032 ->where_group(
1033 function ( $q ) use ( $start_date, $end_date ) {
1034 $q->where( 'custom_date', '=', '0' )
1035 ->or_where_group(
1036 function ( $q2 ) use ( $start_date, $end_date ) {
1037 $q2->where( 'custom_date', '>', $start_date )
1038 ->where( 'custom_date', '<', $end_date );
1039 }
1040 );
1041 }
1042 )
1043 ->to_array();
1044 $team_member_schedules = new Team_Member_Schedule( $team_member_schedules, $team_member_id );
1045
1046 $default_schedules = $db->table( booktics_get_table_name( 'booktics_schedules' ) )
1047 ->where( 'service_id', '=', 0 )
1048 ->where( 'team_member_id', '=', 0 )
1049 ->where_group(
1050 function ( $q ) use ( $start_date, $end_date ) {
1051 $q->where( 'custom_date', '=', '0' )
1052 ->or_where_group(
1053 function ( $q2 ) use ( $start_date, $end_date ) {
1054 $q2->where( 'custom_date', '>', $start_date )
1055 ->where( 'custom_date', '<', $end_date );
1056 }
1057 );
1058 }
1059 )
1060 ->to_array();
1061 $default_schedules = new Default_Schedule( $default_schedules );
1062
1063 $service = Service_Model::find( $service_id );
1064 $service_duration = $this->get_service_duration_from_additional_durations( $service->additional_durations, $additional_duration_id );
1065 $service_duration = apply_filters( 'booktics_service_duration_for_appointment_time_slots', $service_duration, $request );
1066 $slot_length = $service->interval_time < 1 ? booktics_get_option( 'interval_time', 30 ) : $service->interval_time;
1067
1068 // Get buffer times from service
1069 $before_buffer_time = $service->before_buffer_time ? intval( $service->before_buffer_time ) : 0;
1070 $after_buffer_time = $service->after_buffer_time ? intval( $service->after_buffer_time ) : 0;
1071 $options = array(
1072 'default_schedule' => $default_schedules,
1073 'service_duration' => $service_duration,
1074 'slot_length' => $slot_length,
1075 'before_buffer_time' => $before_buffer_time,
1076 'after_buffer_time' => $after_buffer_time,
1077 'service_id' => $service_id,
1078 'total_attendees' => $total_attendees,
1079 );
1080 // Generate time slots for the given date range by interval time
1081 $slots = ( new Time_Slot_Manager( $service_schedules, $team_member_schedules, $options ) )
1082 ->generate_slots_for_range( $start_date, $end_date );
1083
1084
1085 $response = array(
1086 'service_id' => $service_id,
1087 'team_member_id' => $team_member_id,
1088 'additional_duration_id' => $additional_duration_id,
1089 'start_date' => $start_date,
1090 'end_date' => $end_date,
1091 'slots' => $slots,
1092 );
1093
1094 return $this->response( $response, __( 'Successfully fetched time slots', 'booktics' ) );
1095 }
1096
1097 /**
1098 * Get service duration from additional durations using additional_duration_id
1099 * If additional_duration_id is not found, return default duration (30 minutes)
1100 *
1101 * @param array $additional_durations Array of additional durations.
1102 * @param int $additional_duration_id ID of additional duration.
1103 *
1104 * @return int Service duration in minutes.
1105 */
1106 private function get_service_duration_from_additional_durations( $additional_durations, $additional_duration_id ) {
1107 $default_duration = 30;
1108 if ( empty( $additional_durations ) ) {
1109 return $default_duration; // Return default duration if additional durations are empty
1110 }
1111
1112 foreach ( $additional_durations as $additional_duration ) {
1113 if ( $additional_duration['id'] === $additional_duration_id ) {
1114 return $this->get_converted_duration( $additional_duration['duration_time'], $additional_duration['duration_unit'] );
1115 }
1116 }
1117
1118 return $default_duration;
1119 }
1120
1121 /**
1122 * Convert duration to minutes
1123 *
1124 * @param int $duration Duration in minutes or hours.
1125 * @param string $unit Unit of duration (min or hour).
1126 *
1127 * @return int Duration in minutes.
1128 */
1129 private function get_converted_duration($duration, $unit) {
1130 if ( $unit === 'min' ) {
1131 return $duration;
1132 } elseif ( $unit === 'hour' ) {
1133 return $duration * 60;
1134 }
1135 }
1136 }
1137