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

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