PluginProbe
Booktics – Appointment Booking Calendar for Service Businesses / 1.0.28
Booktics – Appointment Booking Calendar for Service Businesses v1.0.28
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 / service / controllers / service-controller.php

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

717 lines 26.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Booktics\Service\Controllers;
4
5 use Booktics\Abstracts\Base_Rest_Controller;
6 use Booktics\Models\Service_Model;
7 use Booktics\Models\Team_Member_Model;
8 use Booktics\Services\Schedule_Manager;
9 use Booktics\Services\Schedule_Record;
10 use Booktics\Taxonomy\Category;
11 use Booktics\Service\Helpers\Service_Helper;
12 use Exception;
13 use WP_Error;
14 use WP_HTTP_Response;
15 use WP_REST_Server;
16
17 /**
18 * Service controller
19 *
20 * @package Booktics/Service
21 */
22 class Service_Controller extends Base_Rest_Controller {
23 /**
24 * Endpoint namespace
25 *
26 * @var string
27 */
28 protected $namespace = 'booktics/v1';
29
30 /**
31 * Route name
32 *
33 * @var string
34 */
35 protected $base = 'services';
36
37 /**
38 * Register routes
39 *
40 * @return void
41 */
42 public function register_routes(): void {
43 register_rest_route(
44 $this->namespace, $this->base, array(
45 array(
46 'methods' => WP_REST_Server::CREATABLE,
47 'callback' => array( $this, 'create_item' ),
48 'permission_callback' => array(
49 $this,
50 'create_item_permissions_check',
51 ),
52 ),
53 )
54 );
55
56 register_rest_route(
57 $this->namespace, $this->base, array(
58 array(
59 'methods' => WP_REST_Server::READABLE,
60 'callback' => array( $this, 'get_items' ),
61 'permission_callback' => array(
62 $this,
63 'get_items_permissions_check',
64 ),
65 ),
66 )
67 );
68
69 register_rest_route(
70 $this->namespace, $this->base, array(
71 array(
72 'methods' => WP_REST_Server::DELETABLE,
73 'callback' => array( $this, 'delete_items' ),
74 'permission_callback' => array(
75 $this,
76 'delete_items_permissions_check',
77 ),
78 ),
79 )
80 );
81
82 register_rest_route(
83 $this->namespace, $this->base . '/(?P<id>[\d]+)', array(
84 array(
85 'methods' => WP_REST_Server::READABLE,
86 'callback' => array( $this, 'get_item' ),
87 'permission_callback' => array(
88 $this,
89 'get_item_permissions_check',
90 ),
91 ),
92 )
93 );
94
95 register_rest_route(
96 $this->namespace, $this->base . '/(?P<id>[\d]+)', array(
97 array(
98 'methods' => WP_REST_Server::EDITABLE,
99 'callback' => array( $this, 'update_item' ),
100 'permission_callback' => array(
101 $this,
102 'update_item_permissions_check',
103 ),
104 ),
105 )
106 );
107
108 register_rest_route(
109 $this->namespace, $this->base . '/(?P<id>[\d]+)', array(
110 array(
111 'methods' => WP_REST_Server::DELETABLE,
112 'callback' => array( $this, 'delete_item' ),
113 'permission_callback' => array(
114 $this,
115 'delete_item_permissions_check',
116 ),
117 ),
118 )
119 );
120
121 register_rest_route(
122 $this->namespace, $this->base . '/(?P<id>[\d]+)/clone', array(
123 array(
124 'methods' => WP_REST_Server::CREATABLE,
125 'callback' => array( $this, 'clone_item' ),
126 'permission_callback' => array(
127 $this,
128 'create_item_permissions_check',
129 ),
130 ),
131 )
132 );
133 }
134
135 /**
136 * Get one item from the collection.
137 *
138 * @param $request
139 *
140 * @return WP_HTTP_Response
141 */
142 public function get_item( $request ) {
143 $id = intval( $request['id'] );
144
145 $service = Service_Model::find( $id );
146
147 if ( ! $service ) {
148 return $this->error( __( 'Service not found', 'booktics' ), 404 );
149 }
150 $service = $service->to_array();
151
152 $team_member_ids = is_array( $service['team_member'] )
153 ? array_unique( $service['team_member'] ) : array( $service['team_member'] );
154 $service['team_member_details'] = Service_Helper::team_member_details( $team_member_ids );
155 $service['team_member'] = $team_member_ids;
156
157 $category_ids = $this->get_service_category_ids( $service['id'] );
158 $service['category_details'] = Service_Helper::category_details( $category_ids );
159 $service['categories'] = $category_ids;
160
161 $schedule_record = ( new Schedule_Record() )
162 ->get_schedules( $id, 'service' );
163 $service['schedule'] = empty( $schedule_record['schedule'] ) ? (object) array()
164 : $schedule_record['schedule'];
165
166 $service['custom'] = $schedule_record['custom'];
167 $service['holiday'] = $schedule_record['holiday'];
168 $service['shortcode'] = '[booktics_service_view id="' . $service['id'] . '"]';
169 $service = apply_filters( 'booktics_service_format', $service );
170
171 return $this->response( $service );
172 }
173
174 /**
175 * Check if a given request has access to get a specific item.
176 *
177 * @param $request
178 *
179 * @return WP_Error
180 */
181 public function get_item_permissions_check( $request ): bool {
182 return true;
183 }
184
185 /**
186 * Get a collection of items.
187 *
188 * @param $request
189 *
190 * @return WP_HTTP_Response
191 */
192 public function get_items( $request ) {
193 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 10;
194 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
195
196 $args = array(
197 'per_page' => $per_page,
198 'paged' => $paged,
199 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for filtering services by meta fields
200 'meta_query' => array(),
201 );
202
203 if ( ! empty( $request['team_member_id'] ) ) {
204 $args['meta_query'][] = array(
205 'key' => 'team_member',
206 'value' => sprintf( 'i:%d;', intval( $request['team_member_id'] ) ),
207 'compare' => 'LIKE',
208 );
209 }
210
211 if ( ! empty( $request['search'] ) ) {
212 $args['s'] = sanitize_text_field( $request['search'] );
213 }
214
215 if ( ! empty( $args['meta_query'] ) ) {
216 $args['meta_query']['relation'] = 'AND';
217 }
218
219 $args = apply_filters( 'booktics_service_args', $args, $request );
220
221 // Get service IDs for category BEFORE pagination to ensure correct total count
222 $service_ids_for_category = null;
223 if ( ! empty( $request['category_id'] ) ) {
224 $service_ids_for_category = get_term_meta( absint( $request['category_id'] ), 'booktics_category_services', true );
225
226 // If category filter is applied, add post__in to args to filter BEFORE pagination
227 if ( ! empty( $service_ids_for_category ) && is_array( $service_ids_for_category ) ) {
228 $args['post__in'] = $service_ids_for_category;
229 } elseif ( ! empty( $service_ids_for_category ) ) {
230 // If category has no services, return early
231 return $this->response( array(
232 'items' => array(),
233 'total' => 0,
234 'per_page' => $per_page,
235 'current_page' => $paged,
236 'last_page' => 0,
237 ) );
238 }
239 }
240
241 $data = Service_Model::paginate( $per_page, $paged, $args );
242
243 if ( ! $data ) {
244 return $this->error( __( 'No service found', 'booktics' ), 404 );
245 }
246
247 if ( ! empty( $data['items'] ) ) {
248 $enhanced_items = array();
249 foreach ( $data['items'] as $service ) {
250 $service_array = $service->to_array();
251
252 if ( ! empty( $service_array['team_member'] ) ) {
253 $team_member_ids = is_array( $service->team_member ) ? array_unique( $service->team_member ) : array( $service->team_member );
254 $service->team_member = $team_member_ids;
255 $service_array['team_member_details'] = Service_Helper::team_member_details( $team_member_ids );
256 }
257 $category_ids = $this->get_service_category_ids( $service_array['id'] );
258 if ( ! empty( $category_ids ) ) {
259 $service->categories = $category_ids;
260 $service_array['category_details'] = Service_Helper::category_details( $category_ids );
261 }
262 $service_array['shortcode'] = '[booktics_service_view id="' . $service_array['id'] . '"]';
263 $service_array = apply_filters( 'booktics_service_format', $service_array );
264 $enhanced_items[] = $service_array;
265 }
266 $data['items'] = $enhanced_items;
267 }
268
269 return $this->response( $data );
270 }
271
272 /**
273 * Get all category IDs linked to a given service
274 *
275 * @param int $service_id
276 * @return array
277 */
278 private function get_service_category_ids( int $service_id ): array {
279 $args = array(
280 'taxonomy' => 'booktics_service_category', // Category taxonomy
281 'hide_empty' => false,
282 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Meta query is necessary for services by category ID
283 'meta_query' => array(
284 array(
285 'key' => 'booktics_category_services',
286 'value' => sprintf( 'i:%d;', $service_id ),
287 'compare' => 'LIKE',
288 ),
289 ),
290 'fields' => 'ids', // returning only ids
291 );
292
293 $categories = get_terms( $args );
294
295 if ( is_wp_error( $categories ) || empty( $categories ) ) {
296 return [];
297 }
298
299 return $categories;
300 }
301
302
303 /**
304 * Check if a given request has access to get items.
305 *
306 * @param $request
307 *
308 * @return WP_Error
309 */
310 public function get_items_permissions_check( $request ): bool {
311 return true;
312 }
313
314 /**
315 * Create one item from the collection.
316 *
317 * @param $request
318 *
319 * @return WP_HTTP_Response
320 * @throws Exception
321 */
322 public function create_item( $request ) {
323 $data = $this->prepare_item_for_database( $request );
324 if ( is_wp_error( $data ) ) {
325 return $this->error( $data->get_error_message() );
326 }
327
328 $service = Service_Model::create( $data );
329
330 do_action( 'booktics_service_created', $service, $request );
331
332 if ( ! empty( $data['team_member'] ) && is_array( $data['team_member'] ) ) {
333 $this->update_team_members( $service->id, $data['team_member'] );
334 $this->remove_service_from_other_team_members( $service->id, $data['team_member'] );
335 }
336
337 if ( is_wp_error( $service ) ) {
338 return $this->error( $service->get_error_message() );
339 }
340 $service = $service->to_array();
341 $service['shortcode'] = '[booktics_service_view id="' . $service['id'] . '"]';
342 $service['category_details'] = Service_Helper::category_details( $service['categories'] );
343 $service['team_member_details'] = Service_Helper::team_member_details( $service['team_member'] );
344 $service = apply_filters( 'booktics_service_format', $service );
345
346 return $this->response( $service, __( 'Successfully created service', 'booktics' ) );
347 }
348
349 /**
350 * Check if a given request has access to create items.
351 *
352 * @param $request
353 *
354 * @return WP_Error
355 */
356 public function create_item_permissions_check( $request ): bool {
357 return current_user_can( 'manage_options' );
358 }
359
360 /**
361 * Update one item from the collection.
362 *
363 * @param $request
364 *
365 * @return WP_HTTP_Response
366 * @throws Exception
367 */
368 public function update_item( $request ) {
369 $id = (int) $request->get_param( 'id' );
370 $data = $this->prepare_item_for_database( $request );
371
372 if ( is_wp_error( $data ) ) {
373 return $this->error( $data->get_error_message() );
374 }
375
376 $service = Service_Model::find( $id );
377
378 if ( ! $service ) {
379 return $this->error( __( 'Service not found', 'booktics' ), 404 );
380 }
381
382 $result = $service->update( $data );
383
384 if ( is_wp_error( $result ) ) {
385 return $this->error( $result->get_error_message() );
386 }
387
388 if ( ! empty( $data['team_member'] ) && is_array( $data['team_member'] ) ) {
389 $this->update_team_members( $id, $data['team_member'] );
390 $this->remove_service_from_other_team_members( $id, $data['team_member'] );
391 }
392
393 $input_data = json_decode( $request->get_body(), true );
394 $this->update_service_schedule( $id, $input_data );
395
396 do_action( 'booktics_service_updated', $service, $request );
397
398 $response = $this->format_service_response( $id );
399 return $this->response( $response, __( 'Successfully updated service', 'booktics' ) );
400 }
401
402 /**
403 * Update team members for a service
404 */
405 private function update_team_members( $service_id, $team_members ) {
406 foreach ( $team_members as $team_member_id ) {
407 $team_member_model = ( new Team_Member_Model( $team_member_id ) );
408 $team_member = $team_member_model->find( $team_member_id );
409 $team_member_services = $team_member->services == '' ? array() : $team_member->services;
410
411 if ( ! in_array( (int) $service_id, $team_member_services ) ) {
412 $team_member_services[] = (int) $service_id;
413 if ( $team_member ) {
414 $team_member->update(
415 array(
416 'services' => $team_member_services,
417 )
418 );
419 }
420 }
421 }
422 }
423
424 /**
425 * Remove service from other team members not in the list
426 */
427 private function remove_service_from_other_team_members( $service_id, $team_members ) {
428 $other_team_members = ( new Team_Member_Model() )
429 ->where( 'services', $service_id, 'LIKE' )
430 ->all( array() );
431
432 if ( ! empty( $other_team_members['items'] ) ) {
433 foreach ( $other_team_members['items'] as $other_team_member ) {
434 if ( ! in_array( $other_team_member['id'], $team_members ) ) {
435 $other_services = $other_team_member['services'];
436 if ( ( $key = array_search( $service_id, $other_services ) ) !== false ) {
437 unset( $other_services[ $key ] );
438 ( new Team_Member_Model( $other_team_member['id'] ) )->update(
439 array(
440 'services' => array_values( $other_services ),
441 )
442 );
443 }
444 }
445 }
446 }
447 }
448
449 /**
450 * Update the service schedule if provided
451 */
452 private function update_service_schedule( $service_id, $input_data ) {
453 if ( isset( $input_data['schedule'] ) || isset( $input_data['custom'] ) || isset( $input_data['holiday'] ) ) {
454 $service_schedule = Schedule_Manager::make( $input_data );
455 $service_schedule->with_service_id( $service_id );
456 $service_schedule->save();
457 }
458 }
459
460 /**
461 * Format the service response after update
462 */
463 private function format_service_response( $service_id ) {
464 $response = Service_Model::find( $service_id )->to_array();
465 $response['shortcode'] = '[booktics_service_view id="' . $response['id'] . '"]';
466 $response['team_member_details'] = Service_Helper::team_member_details( $response['team_member'] );
467 if ( ! empty( $response['categories'] ) ) {
468 $category_ids = is_array( $response['categories'] ) ? $response['categories'] : array( $response['categories'] );
469 $response['category_details'] = Service_Helper::category_details( $category_ids );
470 }
471 return apply_filters( 'booktics_service_format', $response );
472 }
473
474 /**
475 * Check if a given request has access to update a specific item.
476 *
477 * @param $request
478 *
479 * @return WP_Error
480 */
481 public function update_item_permissions_check( $request ) {
482 return current_user_can( 'manage_options' );
483 }
484
485 /**
486 * Delete one item from the collection.
487 *
488 * @param $request
489 *
490 * @return WP_HTTP_Response
491 */
492 public function delete_item( $request ) {
493 $id = intval( $request['id'] );
494
495 $service = Service_Model::find( $id );
496
497 if ( ! $service ) {
498 return $this->error( __( 'Service not found', 'booktics' ), 404 );
499 }
500
501 $deleted = $service->delete();
502
503 if ( is_wp_error( $deleted ) ) {
504 return $this->error( $deleted->get_error_message() );
505 }
506
507 return $this->response( $service, __( 'Successfully deleted service', 'booktics' ) );
508 }
509
510 /**
511 * Check if a given request has access to delete a specific item.
512 *
513 * @param $request
514 *
515 * @return WP_Error
516 */
517 public function delete_item_permissions_check( $request ) {
518 return current_user_can( 'manage_options' );
519 }
520
521 /**
522 * Delete a collection of items
523 *
524 * @param $request
525 *
526 * @return WP_HTTP_Response
527 */
528 public function delete_items( $request ) {
529 $ids = isset( $request['ids'] ) ? $request['ids'] : array();
530
531 if ( ! is_array( $ids ) ) {
532 return $this->error( __( 'Ids should be an array of items ids', 'booktics' ) );
533 }
534
535 $count = 0;
536 $total = count( $ids );
537
538 foreach ( $ids as $id ) {
539 $service = Service_Model::find( $id );
540
541 if ( $service ) {
542 $service->delete();
543 ++$count;
544 }
545 }
546
547 if ( 0 === $count ) {
548 return $this->error( __( 'No item deleted.', 'booktics' ) );
549 }
550 /* translators: 1: deleted count, 2: total count */
551 $message = sprintf( __( 'Deleted %1$d items of %2$d', 'booktics' ), $count, $total );
552
553 return $this->response( array(), $message );
554 }
555
556 /**
557 * Check if a given request has access to delete a collection of items.
558 *
559 * @param $request
560 *
561 * @return WP_Error
562 */
563 public function delete_items_permissions_check( $request ) {
564 return current_user_can( 'manage_options' );
565 }
566
567 /**
568 * Prepare the item for create or update operation.
569 *
570 * @param $request
571 *
572 * @return WP_Error
573 */
574 public function prepare_item_for_database( $request ) {
575 $data = json_decode( $request->get_body(), true );
576 $validate = booktics_validate(
577 $data, array(
578 'title' => array(
579 'required',
580 ),
581 )
582 );
583
584 if ( is_wp_error( $validate ) ) {
585 return $validate;
586 }
587 if ( empty( $data['team_member'] ) ) {
588 $admin_query = new \WP_User_Query(
589 array(
590 'role' => array( 'administrator', 'booktics_team_member' ),
591 )
592 );
593 $admins = $admin_query->get_results();
594 $admin = reset( $admins );
595 $team_member_id = $admin->id;
596 $data['team_member'] = array( $team_member_id );
597 }
598
599 $data['title'] = ! empty( $data['title'] ) ? sanitize_text_field( $data['title'] ) : '';
600 $data['description'] = ! empty( $data['description'] ) ? sanitize_text_field( $data['description'] ) : '';
601 $data['content'] = ! empty( $data['content'] ) ? sanitize_text_field( $data['content'] ) : '';
602 $data['link'] = ! empty( $data['link'] ) ? sanitize_text_field( $data['link'] ) : '';
603 $data['image'] = ! empty( $data['image'] ) ? esc_url_raw( $data['image'] ) : '';
604 $data['image_id'] = ! empty( $data['image_id'] ) ? esc_url_raw( $data['image_id'] ) : '';
605 $data['interval_time'] = ! empty( $data['interval_time'] ) ? sanitize_text_field( $data['interval_time'] ) : 0;
606 $data['before_buffer_time'] = ! empty( $data['before_buffer_time'] ) ? sanitize_text_field( $data['before_buffer_time'] ) : 0;
607 $data['after_buffer_time'] = ! empty( $data['after_buffer_time'] ) ? sanitize_text_field( $data['after_buffer_time'] ) : 0;
608 $data['show_end_time'] = ! empty( $data['show_end_time'] ) ? sanitize_text_field( $data['show_end_time'] ) : false;
609 $data['status'] = ! empty( $data['status'] ) ? sanitize_text_field( $data['status'] ) : 'deactive';
610 $data['team_member'] = empty( $data['team_member'] ) ? array() : array_unique( $data['team_member'] );
611 $data['categories'] = empty( $data['categories'] ) ? array() : array_unique( $data['categories'] );
612 $sanitized_durations = array();
613 if ( ! empty( $data['additional_durations'] ) ) {
614 foreach ( $data['additional_durations'] as $duration ) {
615 if ( is_array( $duration ) ) {
616 $sanitized_durations[] = array(
617 'id' => ! empty( $duration['id'] ) ? sanitize_text_field( $duration['id'] ) : '',
618 'name' => ! empty( $duration['name'] ) ? sanitize_text_field( $duration['name'] ) : '',
619 'duration_time' => ! empty( $duration['duration_time'] ) ? sanitize_text_field( $duration['duration_time'] ) : 0,
620 'duration_unit' => ! empty( $duration['duration_unit'] ) ? sanitize_text_field( $duration['duration_unit'] ) : '',
621 'charge_amount' => ! empty( $duration['charge_amount'] ) ? (float) number_format( (float) $duration['charge_amount'], 4, '.', '' ) : '0.0000',
622 'deposit_amount' => ! empty( $duration['deposit_amount'] ) ? (float) number_format( (float) $duration['deposit_amount'], 4, '.', '' ) : '0.0000',
623 );
624 }
625 }
626 } else {
627 $sanitized_durations = array(
628 array(
629 'id' => 'default',
630 'name' => 'default',
631 'duration_time' => 30,
632 'duration_unit' => 'min',
633 'charge_amount' => 0,
634 'deposit_amount' => 0,
635 ),
636 );
637 }
638 $data['additional_durations'] = $sanitized_durations;
639 $data = apply_filters( 'booktics_prepare_service_custom_fields', $data );
640 $image_url = $request->get_param( 'image' );
641 if ( $image_url ) {
642 $data['image'] = esc_url_raw( $image_url );
643 }
644
645 return $data;
646 }
647
648 /**
649 * Clone service from the record.
650 *
651 * @param $request
652 *
653 * @return WP_HTTP_Response
654 * @throws Exception
655 */
656 public function clone_item( $request ) {
657 $id = intval( $request['id'] );
658
659 $service = Service_Model::find( $id );
660
661 if ( ! $service ) {
662 return $this->error( __( 'Service not found', 'booktics' ), 404 );
663 }
664 $clone_data = [
665 'title' => $service->title . __( ' Copy', 'booktics' ),
666 'description' => $service->description,
667 'content' => $service->content,
668 'image' => $service->image,
669 'image_id' => $service->image_id,
670 'categories' => $service->categories,
671 'interval_time' => $service->interval_time,
672 'before_buffer_time' => $service->before_buffer_time,
673 'after_buffer_time' => $service->after_buffer_time,
674 'team_member' => $service->team_member,
675 'show_end_time' => $service->show_end_time,
676 'additional_durations' => $service->additional_durations,
677 'enable_custom_holiday' => $service->enable_custom_holiday,
678 'enable_custom_schedule' => $service->enable_custom_schedule,
679 'enable_additional_service_duration' => $service->enable_additional_service_duration,
680 ];
681
682 $clone_data = apply_filters( 'booktics_clone_service_data', $clone_data, $service );
683
684 $cloned = Service_Model::create( $clone_data );
685
686 if ( is_wp_error( $cloned ) ) {
687 return $this->error( $cloned->get_error_message() );
688 }
689
690 $schedule_record = ( new Schedule_Record() )
691 ->get_schedules( $id, 'service' );
692 $schedule_data['schedule'] = $schedule_record['schedule'];
693 $schedule_data['custom'] = $schedule_record['custom'];
694 $schedule_data['holiday'] = $schedule_record['holiday'];
695
696 $service_schedule = Schedule_Manager::make( $schedule_data );
697 $service_schedule->with_service_id( $cloned->id );
698 $service_schedule->save();
699
700 $team_member_details = Service_Helper::team_member_details( $service->team_member );
701 $cloned = $cloned->to_array();
702 $cloned['team_member_details'] = $team_member_details;
703 $cloned['category_details'] = Service_Helper::category_details( $cloned['categories'] );
704
705 $clone_schedule_record = ( new Schedule_Record() )
706 ->get_schedules( $cloned['id'], 'service' );
707 $cloned['schedule'] = empty( $clone_schedule_record['schedule'] ) ? (object) array()
708 : $clone_schedule_record['schedule'];
709 $cloned['custom'] = $clone_schedule_record['custom'];
710 $cloned['holiday'] = $clone_schedule_record['holiday'];
711 $cloned['shortcode'] = '[booktics_service_view id="' . $cloned['id'] . '"]';
712 $cloned = apply_filters( 'booktics_service_format', $cloned );
713
714 return $this->response( $cloned, __( 'Successfully cloned service', 'booktics' ) );
715 }
716 }
717