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

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