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

1,007 lines 36.6 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 $args = array( 'meta_query' => array() );
233
234 if ( $search ) {
235 $service_ids = array();
236 $customer_ids = array();
237 $team_member_ids = array();
238 // Search Services
239 $services = Service_Model::all(
240 array(
241 's' => $search,
242 )
243 );
244 $service_ids = array_map(
245 function ( $service ) {
246 return $service->id;
247 }, $services
248 );
249
250 // Search Customers
251 $customer_query = new \WP_User_Query(
252 array(
253 'search' => '*' . esc_attr( $search ) . '*',
254 'search_columns' => array( 'display_name', 'user_email' ),
255 'role' => 'booktics_customer',
256 'fields' => 'ID',
257 )
258 );
259 $customer_ids = $customer_query->get_results();
260
261 // Search Team Members
262 $team_member_query = new \WP_User_Query(
263 array(
264 'search' => '*' . esc_attr( $search ) . '*',
265 'search_columns' => array( 'display_name', 'user_email' ),
266 'role' => 'booktics_team_member',
267 'fields' => 'ID',
268 )
269 );
270 $team_member_ids = $team_member_query->get_results();
271
272 if ( $service_ids || $customer_ids || $team_member_ids ) {
273 $args['meta_query'] = array( 'relation' => 'OR' );
274 if ( $service_ids ) {
275 $args['meta_query'][] = array(
276 'key' => 'service_id',
277 'value' => $service_ids,
278 'compare' => 'IN',
279 );
280 }
281 if ( $customer_ids ) {
282 $args['meta_query'][] = array(
283 'key' => 'customer_id',
284 'value' => $customer_ids,
285 'compare' => 'IN',
286 );
287 }
288 if ( $team_member_ids ) {
289 $args['meta_query'][] = array(
290 'key' => 'team_member_id',
291 'value' => $team_member_ids,
292 'compare' => 'IN',
293 );
294 }
295 } else {
296 return $this->response(
297 array(
298 'items' => array(),
299 'total' => 0,
300 'per_page' => $per_page,
301 'current_page' => $paged,
302 'last_page' => 0,
303 ), __( 'No appointments found', 'booktics' )
304 );
305 }
306 }
307
308 if ( $date_from ) {
309 $args['meta_query'][] = array(
310 'key' => 'date',
311 'value' => $date_from,
312 'compare' => '>=',
313 'type' => 'DATE',
314 );
315 }
316
317 if ( $service_id ) {
318 $args['meta_query'][] = array(
319 'key' => 'service_id',
320 'value' => $service_id,
321 'compare' => '=',
322 );
323 }
324
325 if ( $customer_id ) {
326 $args['meta_query'][] = array(
327 'key' => 'customer_id',
328 'value' => $customer_id,
329 'compare' => '=',
330 );
331 }
332
333 if ( $payment_status ) {
334 $args['meta_query'][] = array(
335 'key' => 'payment_status',
336 'value' => $payment_status,
337 'compare' => '=',
338 );
339 }
340
341 if ( $team_member_id ) {
342 $args['meta_query'][] = array(
343 'key' => 'team_member_id',
344 'value' => $team_member_id,
345 'compare' => '=',
346 );
347 }
348
349 // Adds filtering by appointment status
350 if ( $status ) {
351 $args['post_status'] = is_array( $status ) ? $status : array( $status );
352 }
353
354 $args = apply_filters( 'booktics_appointment_args', $args );
355
356 $data = Appointment_Model::paginate( $per_page, $paged, $args );
357
358 $data = apply_filters( 'booktics_appointment_data', $data );
359
360 if ( ! $data ) {
361 return $this->error( __( 'No appointment found', 'booktics' ), 404 );
362 }
363
364 return $this->response( $data );
365 }
366
367 /**
368 * Get one item from the collection.
369 *
370 * @param WP_REST_Request $request Full data about the request.
371 *
372 * @return WP_Error|WP_REST_Response
373 */
374 public function get_item( $request ) {
375 $id = intval( $request['id'] );
376
377 $appointment = Appointment_Model::find( $id );
378
379 if ( ! $appointment ) {
380 return $this->error( __( 'Appointment not found', 'booktics' ), 404 );
381 }
382
383 return $this->response( $appointment );
384 }
385
386 /**
387 * Get all calendar items within a date range
388 *
389 * @param WP_REST_Request $request Full data about the request.
390 *
391 * @return WP_Error|WP_REST_Response
392 */
393 public function get_all_calendar_items( $request ) {
394 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 500;
395 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
396
397 // Check if custom date range is provided
398 if ( ! empty( $request['start_date'] ) && ! empty( $request['end_date'] ) ) {
399 $adjusted_start_date = sanitize_text_field( $request['start_date'] );
400 $adjusted_end_date = sanitize_text_field( $request['end_date'] );
401 $month = date( 'Y-m', strtotime( $adjusted_start_date ) );
402 } else {
403 // Fall back to month-based date range with week adjustment
404 $month = ! empty( $request['month'] ) ? sanitize_text_field( $request['month'] ) : gmdate( 'Y-m' );
405 $start_date = gmdate( 'Y-m-01', strtotime( $month ) );
406 $end_date = gmdate( 'Y-m-t', strtotime( $month ) );
407
408 // Get the first day of the week (Sunday = 0, Monday = 1, etc.)
409 $first_day_of_week = get_option( 'start_of_week', 1 );
410
411 // Adjust start date to include days from previous month to complete the first week
412 $start_date_obj = new \DateTime( $start_date );
413 $start_week_day = (int) $start_date_obj->format( 'w' ); // 0 (Sun) to 6 (Sat)
414 $days_to_subtract = ( $start_week_day - $first_day_of_week + 7 ) % 7;
415 $start_date_obj->modify( "-$days_to_subtract days" );
416 $adjusted_start_date = $start_date_obj->format( 'Y-m-d' );
417
418 // Adjust end date to include days from next month to complete the last week
419 $end_date_obj = new \DateTime( $end_date );
420 $end_week_day = (int) $end_date_obj->format( 'w' ); // 0 (Sun) to 6 (Sat)
421 $days_to_add = ( 6 - $end_week_day + $first_day_of_week ) % 7;
422 $end_date_obj->modify( "+$days_to_add days" );
423 $adjusted_end_date = $end_date_obj->format( 'Y-m-d' );
424 }
425
426 // Get all appointments within the date range using meta_query
427 $args = array(
428 'meta_query' => array(
429 'relation' => 'AND',
430 array(
431 'key' => 'date',
432 'value' => array(
433 $adjusted_start_date,
434 $adjusted_end_date,
435 ),
436 'compare' => 'BETWEEN',
437 'type' => 'DATE',
438 ),
439 ),
440 'posts_per_page' => - 1, // Get all appointments in the range
441 'orderby' => 'meta_value',
442 'meta_key' => 'date',
443 'order' => 'ASC',
444 );
445
446 $service_id = ! empty( $request['service_id'] ) ? intval( $request['service_id'] ) : null;
447 $customer_id = ! empty( $request['customer_id'] ) ? intval( $request['customer_id'] ) : null;
448 $payment_status = ! empty( $request['payment_status'] ) ? sanitize_text_field( $request['payment_status'] ) : null;
449 $team_member_id = ! empty( $request['team_member_id'] ) ? intval( $request['team_member_id'] ) : null;
450 $status = ! empty( $request['status'] ) ? sanitize_text_field( $request['status'] ) : null;
451
452 if ( $service_id ) {
453 $args['meta_query'][] = array(
454 'key' => 'service_id',
455 'value' => $service_id,
456 'compare' => '=',
457 );
458 }
459
460 if ( $customer_id ) {
461 $args['meta_query'][] = array(
462 'key' => 'customer_id',
463 'value' => $customer_id,
464 'compare' => '=',
465 );
466 }
467
468 if ( $payment_status ) {
469 $args['meta_query'][] = array(
470 'key' => 'payment_status',
471 'value' => $payment_status,
472 'compare' => '=',
473 );
474 }
475
476 if ( $team_member_id ) {
477 $args['meta_query'][] = array(
478 'key' => 'team_member_id',
479 'value' => $team_member_id,
480 'compare' => '=',
481 );
482 }
483
484 if ( $status ) {
485 $args['meta_query'][] = array(
486 'key' => 'status',
487 'value' => $status,
488 'compare' => '=',
489 );
490 }
491
492 $args = apply_filters( 'booktics_appointment_args', $args );
493
494 $appointments = Appointment_Model::paginate( $per_page, $paged, $args );
495
496 // Format the response
497 $formatted_appointments = array();
498
499 if ( ! empty( $appointments ) ) {
500 foreach ( $appointments['items'] as $appointment ) {
501 if ( ! empty( $appointment ) ) {
502 $appointment->source = 'booktics';
503 }
504
505 $formatted_appointments[] = $appointment;
506 }
507 }
508
509 // Allow other developers to add/modify appointments
510 $formatted_appointments = apply_filters(
511 'booktics_appointments_for_calendar_view',
512 $formatted_appointments,
513 array(
514 'start_date' => $adjusted_start_date,
515 'end_date' => $adjusted_end_date,
516 )
517 );
518
519 $response = array(
520 'month' => gmdate( 'Y-m', strtotime( $month ) ),
521 'date_range' => array(
522 'start' => $adjusted_start_date,
523 'end' => $adjusted_end_date,
524 ),
525 'appointments' => $formatted_appointments,
526 );
527
528 return $this->response( $response, __( 'Calendar appointments retrieved successfully', 'booktics' ) );
529 }
530
531 /**
532 * Check if a given request has access to get a specific item.
533 *
534 * @param WP_REST_Request $request Full data about the request.
535 *
536 * @return WP_Error|boolean
537 */
538 public function get_item_permissions_check( $request ) {
539 return true;
540 }
541
542 /**
543 * Check if a given request has access to get items.
544 *
545 * @param WP_REST_Request $request Full data about the request.
546 *
547 * @return WP_Error|boolean
548 */
549 public function get_items_permissions_check( $request ) {
550 return true;
551 }
552
553 /**
554 * Create one item from the collection.
555 *
556 * @param WP_REST_Request $request Full data about the request.
557 *
558 * @return WP_Error|WP_REST_Response
559 * @throws Exception
560 */
561 public function create_item( $request ) {
562 $data = json_decode( $request->get_body(), true );
563 $error = Appointment_Validator::instance()->validate_create( $data );
564 if ( $error ) {
565 return $this->error( $error->get_error_message() );
566 }
567 $appointment = Appointment_Service::instance()->create( $data );
568 if ( is_wp_error( $appointment ) ) {
569 return $this->error( $appointment->get_error_message() );
570 }
571 do_action( 'booktics_after_appointment_created', $appointment );
572
573 return $this->response( $appointment, __( 'Successfully created appointment', 'booktics' ) );
574 }
575
576 /**
577 * Check if a given request has access to create items.
578 *
579 * @param WP_REST_Request $request Full data about the request.
580 *
581 * @return WP_Error|boolean
582 */
583 public function create_item_permissions_check( $request ) {
584 return current_user_can( 'manage_options' );
585 }
586
587 /**
588 * Update one item from the collection.
589 *
590 * @param WP_REST_Request $request Full data about the request.
591 *
592 * @return WP_REST_Response
593 */
594 public function update_item( $request ) {
595 $id = intval( $request['id'] );
596 $data = json_decode( $request->get_body(), true );
597 $error = Appointment_Validator::instance()->validate_update( $data );
598 if ( $error ) {
599 return $this->error( $error->get_error_message() );
600 }
601 $updated_appointment = Appointment_Service::instance()->update( $id, $data );
602 if ( is_wp_error( $updated_appointment ) ) {
603 return $this->error( $updated_appointment->get_error_message() );
604 }
605 do_action( 'booktics_after_appointment_updated', $updated_appointment );
606
607 return $this->response( $updated_appointment, __( 'Appointment updated successfully', 'booktics' ) );
608 }
609
610 /**
611 * Check if a given request has access to update a specific item.
612 *
613 * @param WP_REST_Request $request Full data about the request.
614 *
615 * @return WP_Error|boolean
616 */
617 public function update_item_permissions_check( $request ) {
618 return current_user_can( 'manage_options' );
619 }
620
621 /**
622 * Delete one item from the collection.
623 *
624 * @param WP_REST_Request $request Full data about the request.
625 *
626 * @return \WP_Error|\WP_HTTP_Response
627 */
628 public function delete_item( $request ) {
629 $id = intval( $request['id'] );
630 $result = Appointment_Service::instance()->delete( $id );
631 if ( is_wp_error( $result ) ) {
632 return $this->error( $result->get_error_message() );
633 }
634 do_action( 'booktics_after_appointment_deleted', $result );
635
636 return $this->response( $result, __( 'Successfully deleted appointment', 'booktics' ) );
637 }
638
639 /**
640 * Check if a given request has access to delete a specific item.
641 *
642 * @param WP_REST_Request $request Full data about the request.
643 *
644 * @return WP_Error|boolean
645 */
646 public function delete_item_permissions_check( $request ) {
647 return current_user_can( 'manage_options' );
648 }
649
650 /**
651 * Delete a collection of items
652 *
653 * @param WP_Rest_Request $request User request
654 */
655 public function delete_items( $request ) {
656 $ids = $request['ids'];
657 $result = Appointment_Service::instance()->delete_many( $ids );
658 if ( is_wp_error( $result ) ) {
659 return $this->error( $result->get_error_message() );
660 }
661
662 return $this->response( $result['data'], $result['message'] );
663 }
664
665 /**
666 * Check if a given request has access to delete a collection of items.
667 *
668 * @param WP_REST_Request $request Full data about the request.
669 *
670 * @return WP_Error|boolean
671 */
672 public function delete_items_permissions_check( $request ) {
673 return current_user_can( 'manage_options' );
674 }
675
676 /**
677 * Reschedule an appointment
678 *
679 * @param $request Full data about the request.
680 *
681 * @throws Exception
682 */
683 public function reschedule_item( $request ) {
684 $id = intval( $request['id'] );
685 $data = json_decode( $request->get_body(), true );
686 $error = Appointment_Validator::instance()->validate_reschedule( $data, $id );
687 if ( $error ) {
688 return $this->error( $error->get_error_message() );
689 }
690 $result = Appointment_Service::instance()->reschedule( $id, $data );
691 if ( is_wp_error( $result ) ) {
692 return $this->error( $result->get_error_message() );
693 }
694
695 $this->send_reschedule_email( $result );
696
697 do_action( 'booktics_after_appointment_rescheduled', $result );
698
699 return $this->response( $result, __( 'Successfully rescheduled appointment', 'booktics' ) );
700 }
701
702 /**
703 * Check if a given request has access to reschedule a specific item.
704 *
705 * @param $request Full data about the request.
706 */
707 public function reschedule_item_permissions_check( $request ) {
708 return true;
709 }
710
711 /**
712 * Send reschedule email to customer
713 *
714 * @param Appointment_Model $appointment Appointment model.
715 */
716 private function send_reschedule_email( $appointment ) {
717 $db = new Booktics_Database();
718 $customer = ( new Guest_Model( $db ) )->find( [ 'id' => $appointment->customer_id ] );
719 $team_member = new Team_Member_Model( $appointment->team_member_id );
720
721 $appointment_reschedule_email_data = [
722 'date' => $appointment->date ?? '',
723 'start_time' => $appointment->start_time ?? '',
724 'end_time' => $appointment->end_time ?? '',
725 'customer_name' => $customer->name ?? '',
726 'user_email' => $customer->email ?? '',
727 'appointment_service' => $appointment->title ?? '',
728 'appointment_team_member' => $team_member->display_name ?? '',
729 'appointment_team_member_email' => $team_member->user_email ?? '',
730 ];
731
732 $appointment_reschedule_email_data = apply_filters( 'booktics_appointment_reschedule_email_data', $appointment_reschedule_email_data );
733
734 do_action( 'global_notification_hook', 'appointment_rescheduled', $appointment_reschedule_email_data );
735 }
736
737 /**
738 * Cancel an appointment
739 *
740 * @param WP_REST_Request $request Full data about the request.
741 *
742 * @throws Exception
743 */
744 public function cancel_item( $request ) {
745 $id = intval( $request['id'] );
746 $data = json_decode( $request->get_body(), true );
747 $cancel_note = ! empty( $data['cancel_note'] ) ? sanitize_text_field( $data['cancel_note'] ) : '';
748
749 $error = Appointment_Validator::instance()->validate_cancel( $id );
750
751 if ( $error ) {
752 return $this->error( $error->get_error_message() );
753 }
754
755 $result = Appointment_Service::instance()->cancel( $id, [ 'cancel_note' => $cancel_note ] );
756
757 if ( is_wp_error( $result ) ) {
758 return $this->error( $result->get_error_message() );
759 }
760
761 $this->send_cancel_email( $result );
762
763 do_action( 'booktics_after_appointment_cancelled', $result );
764
765 return $this->response( $result, __( 'Successfully cancelled appointment', 'booktics' ) );
766 }
767
768 /**
769 * Send cancel email to customer
770 *
771 * @param Appointment_Model $appointment Appointment model.
772 */
773 private function send_cancel_email( $appointment ) {
774 $db = new Booktics_Database();
775 $customer = ( new Guest_Model( $db ) )->find( [ 'id' => $appointment->customer_id ] );
776 $team_member = new Team_Member_Model( $appointment->team_member_id );
777
778 $appointment_cancel_email_data = [
779 'date' => $appointment->date ?? '',
780 'start_time' => $appointment->start_time ?? '',
781 'end_time' => $appointment->end_time ?? '',
782 'customer_name' => $customer->name ?? '',
783 'user_email' => $customer->email ?? '',
784 'appointment_service' => $appointment->title ?? '',
785 'appointment_team_member' => $team_member->display_name ?? '',
786 'appointment_team_member_email' => $team_member->user_email ?? '',
787 ];
788
789 $appointment_cancel_email_data = apply_filters( 'booktics_appointment_cancel_email_data', $appointment_cancel_email_data );
790
791 do_action( 'global_notification_hook', 'appointment_cancelled', $appointment_cancel_email_data );
792 }
793
794 /**
795 * Check if a given request has access to cancel a specific item.
796 *
797 * @param WP_REST_Request $request Full data about the request.
798 *
799 * @return WP_Error|boolean
800 */
801 public function cancel_item_permissions_check( $request ) {
802 return true;
803 }
804
805 /**
806 * Prepare the item for create or update operation.
807 *
808 * @param WP_REST_Request $request Request object.
809 *
810 * @return bool|\WP_Error $prepared_item
811 */
812 public function prepare_item_for_database( $request ) {
813 $data = json_decode( $request->get_body(), true );
814 $validate = booktics_validate(
815 $data,
816 array(
817 'start_time' => array(
818 'required',
819 ),
820 'end_time' => array(
821 'required',
822 ),
823 'date' => array( 'required' ),
824 'service_id' => array(
825 'required',
826 ),
827 'team_member_id' => array(
828 'required',
829 ),
830 )
831 );
832
833 if ( is_wp_error( $validate ) ) {
834 return $validate;
835 }
836
837 $data = $this->get_sanitized_data( $data );
838
839 return $data;
840 }
841
842 private function get_sanitized_data( $data ) {
843 $data['start_time'] = ! empty( $data['start_time'] ) ? sanitize_text_field( $data['start_time'] ) : '';
844 $data['end_time'] = ! empty( $data['end_time'] ) ? sanitize_text_field( $data['end_time'] ) : '';
845 $data['date'] = ! empty( $data['date'] ) ? sanitize_text_field( $data['date'] ) : '';
846 $data['service_id'] = ! empty( $data['service_id'] ) ? sanitize_text_field( $data['service_id'] ) : '';
847 $data['team_member_id'] = ! empty( $data['team_member_id'] ) ? sanitize_text_field( $data['team_member_id'] ) : '';
848 $data['customer_id'] = ! empty( $data['customer_id'] ) ? sanitize_text_field( $data['customer_id'] ) : '';
849 $data['total_attendees'] = ! empty( $data['total_attendees'] ) ? sanitize_text_field( $data['total_attendees'] ) : '';
850 $data['status'] = isset( $data['status'] ) ? $data['status'] : 'pending';
851
852 return $data;
853 }
854
855 /**
856 * Check if a given request has access to get time slots.
857 *
858 * @param $request
859 *
860 * @return boolean
861 */
862 public function get_time_slots_permissions_check( $request ) {
863 return true;
864 }
865
866 /**
867 * Fetch appointment time slots
868 *
869 * @param WP_REST_Request $request Request object.
870 *
871 * @return array|\WP_HTTP_Response
872 * @throws Exception If there's an error getting time slots.
873 */
874 public function get_time_slots( $request ) {
875 $service_id = $request['service_id'];
876 $additional_duration_id = $request['additional_duration_id'];
877 $team_member_id = $request['team_member_id'];
878 $start_date = $request['start_date'];
879 $end_date = $request['end_date'];
880 $total_attendees = $request['total_attendees'] ?? 1;
881
882 $db = new Booktics_Database();
883
884 $service_schedules = $db->table( booktics_get_table_name( 'booktics_schedules' ) )
885 ->where( 'service_id', '=', $service_id )
886 ->where_group(
887 function ( $q ) use ( $start_date, $end_date ) {
888 $q->where( 'custom_date', '=', '0' )
889 ->or_where_group(
890 function ( $q2 ) use ( $start_date, $end_date ) {
891 $q2->where( 'custom_date', '>', $start_date )
892 ->where( 'custom_date', '<', $end_date );
893 }
894 );
895 }
896 )
897 ->to_array();
898 $service_schedules = new Service_Schedule( $service_schedules, $service_id );
899
900 $team_member_schedules = $db->table( booktics_get_table_name( 'booktics_schedules' ) )
901 ->where( 'team_member_id', '=', $team_member_id )
902 ->where_group(
903 function ( $q ) use ( $start_date, $end_date ) {
904 $q->where( 'custom_date', '=', '0' )
905 ->or_where_group(
906 function ( $q2 ) use ( $start_date, $end_date ) {
907 $q2->where( 'custom_date', '>', $start_date )
908 ->where( 'custom_date', '<', $end_date );
909 }
910 );
911 }
912 )
913 ->to_array();
914 $team_member_schedules = new Team_Member_Schedule( $team_member_schedules, $team_member_id );
915
916 $default_schedules = $db->table( booktics_get_table_name( 'booktics_schedules' ) )
917 ->where( 'service_id', '=', 0 )
918 ->where( 'team_member_id', '=', 0 )
919 ->where_group(
920 function ( $q ) use ( $start_date, $end_date ) {
921 $q->where( 'custom_date', '=', '0' )
922 ->or_where_group(
923 function ( $q2 ) use ( $start_date, $end_date ) {
924 $q2->where( 'custom_date', '>', $start_date )
925 ->where( 'custom_date', '<', $end_date );
926 }
927 );
928 }
929 )
930 ->to_array();
931 $default_schedules = new Default_Schedule( $default_schedules );
932
933 $service = Service_Model::find( $service_id );
934 $service_duration = $this->get_service_duration_from_additional_durations( $service->additional_durations, $additional_duration_id );
935 $service_duration = apply_filters( 'booktics_service_duration_for_appointment_time_slots', $service_duration, $request );
936 $slot_length = $service->interval_time < 1 ? booktics_get_option( 'interval_time', 30 ) : $service->interval_time;
937
938 // Get buffer times from service
939 $before_buffer_time = $service->before_buffer_time ? intval( $service->before_buffer_time ) : 0;
940 $after_buffer_time = $service->after_buffer_time ? intval( $service->after_buffer_time ) : 0;
941 $options = array(
942 'default_schedule' => $default_schedules,
943 'service_duration' => $service_duration,
944 'slot_length' => $slot_length,
945 'before_buffer_time' => $before_buffer_time,
946 'after_buffer_time' => $after_buffer_time,
947 'service_id' => $service_id,
948 'total_attendees' => $total_attendees,
949 );
950 // Generate time slots for the given date range by interval time
951 $slots = ( new Time_Slot_Manager( $service_schedules, $team_member_schedules, $options ) )
952 ->generate_slots_for_range( $start_date, $end_date );
953
954
955 $response = array(
956 'service_id' => $service_id,
957 'team_member_id' => $team_member_id,
958 'additional_duration_id' => $additional_duration_id,
959 'start_date' => $start_date,
960 'end_date' => $end_date,
961 'slots' => $slots,
962 );
963
964 return $this->response( $response, __( 'Successfully fetched time slots', 'booktics' ) );
965 }
966
967 /**
968 * Get service duration from additional durations using additional_duration_id
969 * If additional_duration_id is not found, return default duration (30 minutes)
970 *
971 * @param array $additional_durations Array of additional durations.
972 * @param int $additional_duration_id ID of additional duration.
973 *
974 * @return int Service duration in minutes.
975 */
976 private function get_service_duration_from_additional_durations( $additional_durations, $additional_duration_id ) {
977 $default_duration = 30;
978 if ( empty( $additional_durations ) ) {
979 return $default_duration; // Return default duration if additional durations are empty
980 }
981
982 foreach ( $additional_durations as $additional_duration ) {
983 if ( $additional_duration['id'] === $additional_duration_id ) {
984 return $this->get_converted_duration( $additional_duration['duration_time'], $additional_duration['duration_unit'] );
985 }
986 }
987
988 return $default_duration;
989 }
990
991 /**
992 * Convert duration to minutes
993 *
994 * @param int $duration Duration in minutes or hours.
995 * @param string $unit Unit of duration (min or hour).
996 *
997 * @return int Duration in minutes.
998 */
999 private function get_converted_duration($duration, $unit) {
1000 if ( $unit === 'min' ) {
1001 return $duration;
1002 } elseif ( $unit === 'hour' ) {
1003 return $duration * 60;
1004 }
1005 }
1006 }
1007