PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.63
Timetics – Appointment Booking Calendar & Scheduling v1.0.63
1.0.62 1.0.63 1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 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.19 1.0.2 1.0.20 1.0.21 1.0.22 All 64 releases
timetics / core / appointments / api-appointment.php

api-appointment.php in Timetics – Appointment Booking Calendar & Scheduling 1.0.63, at core/appointments/api-appointment.php

1,167 lines 44.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Apointment api
4 *
5 * @since 1.0.0
6 *
7 * @package Timetics
8 */
9 namespace Timetics\Core\Appointments;
10
11 defined( 'ABSPATH' ) || exit;
12
13 use Timetics\Base\Api;
14 use Timetics\Core\Appointments\Appointment;
15 use Timetics\Core\Staffs\Staff;
16 use Timetics\Utils\Singleton;
17 use WP_Error;
18 use WP_HTTP_Response;
19 use WP_REST_Request;
20
21 /**
22 * Api_Appointment class
23 *
24 * @since 1.0.0
25 */
26 class Api_Appointment extends Api {
27
28 use Singleton;
29
30 /**
31 * Store api namespace
32 *
33 * @since 1.0.0
34 *
35 * @var string $namespace
36 */
37 protected $namespace = 'timetics/v1';
38
39 /**
40 * Store rest base
41 *
42 * @since 1.0.0
43 *
44 * @var string $rest_base
45 */
46 protected $rest_base = 'appointments';
47
48 /**
49 * Register rest routes.
50 *
51 * @since 1.0.0
52 *
53 * @return void
54 */
55 public function register_routes() {
56 /*
57 * Register route
58 */
59 register_rest_route( $this->namespace, $this->rest_base, [
60 [
61 'methods' => \WP_REST_Server::READABLE,
62 'callback' => [$this, 'get_items'],
63 'permission_callback' => function () {
64 return true;
65 },
66 ],
67 [
68 'methods' => \WP_REST_Server::CREATABLE,
69 'callback' => [$this, 'create_item'],
70 'permission_callback' => function () {
71 return current_user_can( 'read_meeting' );
72 },
73 ],
74 [
75 'methods' => \WP_REST_Server::DELETABLE,
76 'callback' => [$this, 'bulk_delete'],
77 'permission_callback' => function () {
78 return current_user_can( 'read_meeting' );
79 },
80 ],
81 ] );
82
83 /**
84 * Register route
85 *
86 * @var void
87 */
88 register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<appointment_id>[\d]+)', [
89 [
90 'methods' => \WP_REST_Server::READABLE,
91 'callback' => [$this, 'get_item'],
92 'permission_callback' => function () {
93 return true;
94 },
95 ],
96 [
97 'methods' => \WP_REST_Server::EDITABLE,
98 'callback' => [$this, 'update_item'],
99 'permission_callback' => [ $this, 'update_item_permissions_check' ],
100 ],
101 [
102 'methods' => \WP_REST_Server::DELETABLE,
103 'callback' => [$this, 'delete_item'],
104 'permission_callback' => [$this, 'delete_item_permissions_check' ],
105 ],
106 ] );
107
108 register_rest_route( $this->namespace, $this->rest_base . '/search', [
109 [
110 'methods' => \WP_REST_Server::READABLE,
111 'callback' => [$this, 'search_items'],
112 'permission_callback' => function () {
113 // edit_meeting is admin-only in this plugin (see get_items()) —
114 // staff need manage_timetics to search their own meetings at all.
115 return current_user_can( 'manage_timetics' ) || current_user_can( 'manage_options' );
116 },
117 ],
118 ] );
119
120 register_rest_route( $this->namespace, $this->rest_base . '/filter', [
121 [
122 'methods' => \WP_REST_Server::READABLE,
123 'callback' => [$this, 'filter_items'],
124 'permission_callback' => function () {
125 return true;
126 },
127 ],
128 ] );
129
130 register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<appointment_id>[\d]+)' . '/duplicate', [
131 [
132 'methods' => \WP_REST_Server::CREATABLE,
133 'callback' => [$this, 'duplicate_item'],
134 'permission_callback' => function () {
135 return current_user_can( 'edit_meeting' );
136 },
137 ],
138 ] );
139 }
140
141 /**
142 * Get all appointments
143 *
144 * @param WP_Rest_Request $request
145 *
146 * @return JSON
147 */
148 public function get_items( $request ) {
149
150 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
151 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
152 $type = ! empty( $request['type'] ) ? sanitize_text_field( $request['type'] ) : '';
153
154 $restrict_to_own = ! current_user_can( 'edit_meeting' );
155 $current_user_id = get_current_user_id();
156
157 if ( $restrict_to_own && ! $current_user_id ) {
158 return rest_ensure_response(
159 [
160 'success' => 1,
161 'status_code' => 200,
162 'data' => [
163 'total' => 0,
164 'items' => [],
165 ],
166 ]
167 );
168 }
169
170 $args = [ 'type' => $type ];
171
172 if ( $restrict_to_own ) {
173 // The staff meta_query is a LIKE match against a serialized array
174 // and isn't safe as the access boundary (staff id 5 also matches
175 // a meeting assigned to staff 55) — fetch broadly and enforce
176 // real ownership below instead of filtering in SQL.
177 $args['posts_per_page'] = -1;
178 } else {
179 $args['posts_per_page'] = $per_page;
180 $args['paged'] = $paged;
181 }
182
183 $appoint = Appointment::all( $args );
184 $matched = $appoint['items'];
185 $total = $appoint['total'];
186
187 if ( $restrict_to_own ) {
188 $matched = array_values(
189 array_filter(
190 $matched,
191 function ( $item ) use ( $current_user_id ) {
192 return in_array( $current_user_id, ( new Appointment( $item->ID ) )->get_staff_ids(), true );
193 }
194 )
195 );
196
197 $total = count( $matched );
198
199 // A per_page value of -1 means "all items". Passing it directly to
200 // array_slice() excludes the last item, which leaves a staff member
201 // with a single assigned meeting with an empty meeting list.
202 if ( -1 !== $per_page ) {
203 $matched = array_slice( $matched, ( $paged - 1 ) * $per_page, $per_page );
204 }
205 }
206
207 $items = [];
208
209 foreach ( $matched as $item ) {
210 $items[] = $this->prepare_item( $item->ID );
211 }
212
213 $data = [
214 'success' => 1,
215 'status_code' => 200,
216 'data' => [
217 'total' => $total,
218 'items' => $items,
219 ],
220 ];
221
222 return rest_ensure_response( $data );
223 }
224
225 /**
226 * Search appointment
227 *
228 * @param Object $request
229 *
230 * @return JSON
231 */
232 public function search_items( $request ) {
233
234 // Prepare search args.
235 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
236 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
237 $search = ! empty( $request['search'] ) ? sanitize_text_field( $request['search'] ) : '';
238 $restrict_to_own = ! current_user_can( 'manage_options' );
239
240 $query_args = array(
241 'post_type' => 'timetics-appointment',
242 'orderby' => 'ID',
243 'order' => 'DESC',
244 );
245
246 if ( $restrict_to_own ) {
247 // Same LIKE-isn't-a-boundary caveat as get_items() — fetch broadly
248 // and enforce real ownership below instead of filtering in SQL.
249 $query_args['posts_per_page'] = -1;
250 } else {
251 $query_args['posts_per_page'] = $per_page;
252 $query_args['paged'] = $paged;
253 }
254
255 // Get search.
256 $appointments = new \WP_Query(
257 array_merge(
258 $query_args,
259 array(
260 // @codingStandardsIgnoreStart
261 'meta_query' => array(
262 'relation' => 'OR',
263 array(
264 'key' => '_tt_apointment_name',
265 'value' => $search,
266 'compare' => 'LIKE',
267 ),
268 array(
269 'key' => '_tt_apointment_type',
270 'value' => $search,
271 'compare' => 'LIKE',
272 ),
273 array(
274 'key' => '_tt_apointment_description',
275 'value' => $search,
276 'compare' => 'LIKE',
277 ),
278 array(
279 'key' => '_tt_apointment_location',
280 'value' => $search,
281 'compare' => 'LIKE',
282 ),
283 array(
284 'key' => '_tt_apointment_duration',
285 'value' => $search,
286 'compare' => 'LIKE',
287 ),
288 array(
289 'key' => '_tt_apointment_schedule',
290 'value' => $search,
291 'compare' => 'LIKE',
292 ),
293 ),
294 // @codingStandardsIgnoreEnd
295 )
296 )
297 );
298
299 $matched = $appointments->posts;
300 $total = $appointments->found_posts;
301
302 if ( $restrict_to_own ) {
303 $current_user_id = get_current_user_id();
304
305 $matched = array_values(
306 array_filter(
307 $matched,
308 function ( $item ) use ( $current_user_id ) {
309 return in_array( $current_user_id, ( new Appointment( $item->ID ) )->get_staff_ids(), true );
310 }
311 )
312 );
313
314 $total = count( $matched );
315
316 if ( -1 !== $per_page ) {
317 $matched = array_slice( $matched, ( $paged - 1 ) * $per_page, $per_page );
318 }
319 }
320
321 // Prepare items for response.
322 $items = [];
323
324 foreach ( $matched as $item ) {
325 $items[] = $this->prepare_item( $item->ID );
326 }
327
328 $data = [
329 'success' => 1,
330 'status' => 200,
331 'data' => [
332 'total' => $total,
333 'items' => $items,
334 ],
335 ];
336
337 return rest_ensure_response( $data );
338 }
339
340 public function filter_items( $request ) {
341
342 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
343 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
344 $staff = ! empty( $request['staff_id'] ) ? intval( $request['staff_id'] ) : '';
345 $category = ! empty( $request['category'] ) ? intval( $request['category'] ) : 0;
346 $visibility = ! empty( $request['visibility'] ) ? sanitize_text_field( $request['visibility'] ) : '';
347
348 $restrict_to_enabled = ! current_user_can( 'edit_meeting' );
349
350 $per_page = ! empty( $request['per_page'] ) ? intval( $request['per_page'] ) : 20;
351 $paged = ! empty( $request['paged'] ) ? intval( $request['paged'] ) : 1;
352
353 $appoint = Appointment::all( [
354 'posts_per_page' => $restrict_to_enabled ? -1 : $per_page,
355 'paged' => $restrict_to_enabled ? 1 : $paged,
356 'visibility' => $visibility,
357 'staff' => $staff,
358 'category' => $category,
359 ] );
360
361 $matched = $appoint['items'];
362 $total = $appoint['total'];
363
364 if ( $restrict_to_enabled ) {
365 // Public route — never show a disabled meeting type, regardless
366 // of what visibility was requested. A blank/missing visibility
367 // meta (legacy rows) is treated as visible, matching the default
368 // used when saving an appointment.
369 $matched = array_values(
370 array_filter(
371 $matched,
372 function ( $item ) {
373 return 'disabled' !== strtolower( (string) ( new Appointment( $item->ID ) )->get_visibility() );
374 }
375 )
376 );
377
378 $total = count( $matched );
379 $matched = array_slice( $matched, ( $paged - 1 ) * $per_page, $per_page );
380 }
381
382 $items = [];
383
384 foreach ( $matched as $item ) {
385 $items[] = $this->prepare_item( $item->ID );
386 }
387
388 $data = [
389 'success' => 1,
390 'status' => 200,
391 'data' => [
392 'total' => $total,
393 'items' => $items,
394 ],
395 ];
396
397 return rest_ensure_response( $data );
398 }
399
400 /**
401 * Create appointment
402 *
403 * @param WP_Rest_Request $request
404 *
405 * @return JSON Newly created appointment data
406 */
407 public function create_item( $request ) {
408
409 /**
410 * Added temporary for leagacy sass. It will remove in future.
411 */
412
413 $meetings_count = Appointment::all();
414 $data = json_decode( $request->get_body(), true );
415
416 $response = [
417 'success' => 0,
418 'status_code' => 403,
419 'message' => esc_html__( 'Something went wrong', 'timetics' ),
420 'data' => [],
421 ];
422
423 if ( ! empty( $data['price'] ) && apply_filters( 'timetics/staff/appointment/price_check', false, $data['price'] ) == true ) {
424 return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'price_check' ), 403 );
425 }
426
427 if ( apply_filters( 'timetics/staff/appointment/count_check', false, $meetings_count ) == true ) {
428 return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'count_check' ), 403 );
429 }
430
431 $type = ! empty( $data['type'] ) ? sanitize_text_field( $data['type'] ) : '';
432
433 if ( apply_filters( 'timetics/staff/appointment/type_check', false, $type ) == true ) {
434 return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'type_check' ), 403 );
435 }
436
437 $categories = ! empty( $data['categories'] ) ? $data['categories'] : '';
438
439 if ( apply_filters( 'timetics/staff/appointment/category_check', false, $categories ) == true ) {
440 return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'category_check' ), 403 );
441 }
442
443 $staff = ! empty( $data['staff'] ) ? array_map( 'intval', $data['staff'] ) : [];
444
445 if ( apply_filters( 'timetics/staff/appointment/staff_check', false, $staff ) == true ) {
446 return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'staff_check' ), 403 );
447 }
448
449 $custom_fields = ! empty( $data['custom_fields'] ) ? $data['custom_fields'] : [];
450
451 if ( apply_filters( 'timetics/staff/appointment/custom_field_check', false, $custom_fields ) == true ) {
452 return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'custom_field_check' ), 403 );
453 }
454
455 $recurring_limit = ! empty( $data['recurring_limit'] ) ? $data['recurring_limit'] : [];
456
457 if ( apply_filters( 'timetics/staff/appointment/recurring_limit_check', false, $recurring_limit ) == true ) {
458
459 return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'recurring_limit_check' ), 403 );
460 }
461
462 // End
463
464 return $this->save_appointment( $request );
465 }
466
467 /**
468 * Update appointment
469 *
470 * @param WP_Rest_Request $request
471 *
472 * @return JSON Updated appointment data
473 */
474 public function update_item( $request ) {
475
476 $appointment_id = (int) $request['appointment_id'];
477 $appoint = new Appointment( $appointment_id );
478
479 // Handler must not rely solely on permission_callback having run.
480 if ( ! $this->can_edit_appointment( $appointment_id ) ) {
481 return new WP_HTTP_Response(
482 [
483 'success' => 0,
484 'status_code' => 403,
485 'message' => esc_html__( 'You are not allowed to edit this appointment.', 'timetics' ),
486 'data' => [],
487 ],
488 403
489 );
490 }
491
492 $data = json_decode( $request->get_body(), true );
493
494 /**
495 * Added temporary for leagacy sass. It will remove in future.
496 */
497 $response = [
498 'success' => 0,
499 'status_code' => 502,
500 'message' => esc_html__( 'Something went wrong', 'timetics' ),
501 'data' => [],
502 ];
503
504 if ( !empty($data['availability']) && $data['availability'] && apply_filters('timetics/staff/meeting/availability', false)) {
505 return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'availability_update' ), 403 );
506 }
507
508 if ( ! empty( $data['price'] ) && apply_filters( 'timetics/staff/appointment/price_check', false, $data['price'] ) == true ) {
509 return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'price_check' ), 403 );
510 }
511
512 $categories = ! empty( $data['categories'] ) ? $data['categories'] : '';
513
514 if ( apply_filters( 'timetics/staff/appointment/category_check', false, $categories ) == true ) {
515 return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'category_check' ), 403 );
516 }
517
518 $staff = ! empty( $data['staff'] ) ? array_map( 'intval', $data['staff'] ) : [];
519
520 if ( apply_filters( 'timetics/staff/appointment/staff_check', false, $staff ) == true ) {
521 return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'staff_check' ), 403 );
522 }
523
524 $custom_fields = ! empty( $data['custom_fields'] ) ? $data['custom_fields'] : [];
525
526 if ( apply_filters( 'timetics/staff/appointment/custom_field_check', false, $custom_fields ) == true ) {
527 return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'custom_field_check' ), 403 );
528 }
529
530 $recurring_limit = ! empty( $data['recurring_limit'] ) ? $data['recurring_limit'] : [];
531
532 if ( apply_filters( 'timetics/staff/appointment/recurring_limit_check', false, $recurring_limit ) == true ) {
533
534 return new WP_HTTP_Response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'recurring_limit_check' ), 403 );
535 }
536
537 // End.
538
539 if ( ! $appoint->is_appointment() ) {
540
541 $response = [
542 'success' => 0,
543 'status_code' => 404,
544 'message' => esc_html__( 'Invalid appointment id.', 'timetics' ),
545 'data' => [],
546 ];
547
548 return new WP_HTTP_Response( $response, 404 );
549 }
550
551 return $this->save_appointment( $request, $appointment_id );
552 }
553
554 /**
555 * Update permission check
556 *
557 * @param WP_Rest_Request $request
558 *
559 * @return bool
560 */
561 public function update_item_permissions_check( $request ) {
562 return $this->can_edit_appointment( (int) $request['appointment_id'] );
563 }
564
565 /**
566 * Object-level authorization for editing an appointment. Called from
567 * both the route's permission_callback and update_item() itself, so
568 * the handler never relies solely on the callback having run.
569 *
570 * @param int $appointment_id
571 *
572 * @return bool
573 */
574 private function can_edit_appointment( $appointment_id ) {
575 if ( current_user_can( 'manage_options' ) ) {
576 return true;
577 }
578
579 $current_user_id = get_current_user_id();
580
581 if ( $current_user_id <= 0 ) {
582 return false;
583 }
584
585 $appointment = new Appointment( $appointment_id );
586
587 if ( ! $appointment->is_appointment() ) {
588 return false;
589 }
590
591 $staff_ids = array_map( 'intval', $appointment->get_staff_ids() );
592 $author = $appointment->get_author();
593
594 // read_meeting only proves "is staff," not ownership — must not bypass the checks below.
595 return in_array( $current_user_id, $staff_ids, true )
596 || $author === $current_user_id;
597 }
598
599 /**
600 * True only for the meeting's owner (author) or an administrator.
601 * Used to gate fields an assigned-but-non-owning staff member must
602 * not be able to change (staff list, visibility, webhooks).
603 *
604 * @param Appointment $appointment
605 *
606 * @return bool
607 */
608 private function is_appointment_owner( $appointment ) {
609 return current_user_can( 'manage_options' )
610 || (int) $appointment->get_author() === get_current_user_id();
611 }
612
613 /**
614 * Get single appointment
615 *
616 * @param WP_Rest_Requesr $request
617 *
618 * @return JSON Single appointment data
619 */
620 public function get_item( $request ) {
621 $appoinment_id = (int) $request['appointment_id'];
622 $appoint = new Appointment( $appoinment_id );
623
624 if ( ! $appoint->is_appointment() ) {
625
626 $data = [
627 'status_code' => 404,
628 'message' => esc_html__( 'Invalid appointment id.', 'timetics' ),
629 'data' => [],
630 ];
631
632 return new WP_HTTP_Response( $data, 404 );
633 }
634
635 $current_user_id = get_current_user_id();
636 $is_privileged = current_user_can( 'edit_meeting' )
637 || $current_user_id == $appoint->get_author()
638 || in_array( $current_user_id, $appoint->get_staff_ids(), true );
639
640 // This route is public — a disabled meeting type isn't meant to be
641 // reachable by guessing its id, only owner/staff/admin can still see it.
642 if ( ! $is_privileged && 'disabled' === strtolower( (string) $appoint->get_visibility() ) ) {
643 $data = [
644 'status_code' => 404,
645 'message' => esc_html__( 'Invalid appointment id.', 'timetics' ),
646 'data' => [],
647 ];
648
649 return new WP_HTTP_Response( $data, 404 );
650 }
651
652 $response = [
653 'status_code' => 200,
654 'message' => esc_html__( 'Successfully retrieved appointments', 'timetics' ),
655 'data' => $this->prepare_item( $appoint ),
656 ];
657
658 return rest_ensure_response( $response );
659 }
660
661 /**
662 * Delete single appointment
663 *
664 * @param WP_Rest_Request $request
665 *
666 * @return
667 */
668 public function delete_item( $request ) {
669
670 $appoinment_id = (int) $request['appointment_id'];
671 $appoint = new Appointment( $appoinment_id );
672
673 $current_user_id = get_current_user_id();
674
675 if ( $appoint->get_author() != $current_user_id ) {
676 $data = [
677 'success' => 0,
678 'message' => __( 'You are not allowed to delete this meeting.', 'timetics' ),
679 ];
680
681 return new WP_HTTP_Response( $data, 403 );
682 }
683
684 if ( ! $appoint->is_appointment() ) {
685 return [
686 'status_code' => 404,
687 'message' => esc_html__( 'Invalid appointment id.', 'timetics' ),
688 'data' => [],
689 ];
690 }
691
692 $appoint->delete();
693
694 $response = [
695 'status_code' => 201,
696 'message' => esc_html__( 'Successfully deleted appointment', 'timetics' ),
697 'data' => [
698 'item' => $appoinment_id,
699 ],
700 ];
701
702 return rest_ensure_response( $response );
703 }
704
705 /**
706 * Delete item permission check
707 *
708 * @param WP_Rest_Request $request
709 *
710 * @return bool
711 */
712 public function delete_item_permissions_check( $request ) {
713 return $this->can_edit_appointment( (int) $request['appointment_id'] );
714 }
715
716 /**
717 * Delete multiples
718 *
719 * @param WP_Rest_Request $request
720 *
721 * @return JSON
722 */
723 public function bulk_delete( $request ) {
724
725 $appointments = json_decode( $request->get_body(), true );
726 $appointments = is_array( $appointments ) ? $appointments : [];
727
728 $current_user_id = get_current_user_id();
729 $is_admin = current_user_can( 'manage_options' );
730
731 $to_delete = [];
732
733 // Validate every id — existence and ownership — before deleting any
734 // of them. The route only checks read_meeting, which every staff
735 // account has, so ownership has to be enforced here per appointment.
736 foreach ( $appointments as $appoint_id ) {
737 $appoint = new Appointment( $appoint_id );
738
739 if ( ! $appoint->is_appointment() ) {
740 $data = [
741 'success' => 0,
742 'status' => 404,
743 'message' => esc_html__( 'Invalid appointment id.', 'timetics' ),
744 'data' => [],
745 ];
746
747 return new WP_HTTP_Response( $data, 404 );
748 }
749
750 if ( ! $is_admin && $appoint->get_author() != $current_user_id ) {
751 $data = [
752 'success' => 0,
753 'status' => 403,
754 'message' => esc_html__( 'You are not allowed to delete one or more of the selected appointments.', 'timetics' ),
755 'data' => [],
756 ];
757
758 return new WP_HTTP_Response( $data, 403 );
759 }
760
761 $to_delete[] = $appoint;
762 }
763
764 foreach ( $to_delete as $appoint ) {
765 $appoint->delete();
766 }
767
768 return rest_ensure_response( [
769 'success' => 1,
770 'status' => 201,
771 'message' => esc_html__( 'Successfully deleted all appointments', 'timetics' ),
772 'data' => [
773 'items' => $appointments,
774 ],
775 ] );
776 }
777
778 /**
779 * Duplicate appointment
780 *
781 * @since 1.0.0
782 *
783 * @param object $request
784 *
785 * @return JSON
786 */
787 public function duplicate_item( $request ) {
788
789 /**
790 * Added temporary for leagacy sass. It will remove in future.
791 */
792
793 $meetings_count = Appointment::all();
794
795 $response = [
796 'success' => 0,
797 'status_code' => 502,
798 'message' => esc_html__( 'Something went wrong', 'timetics' ),
799 'data' => [],
800 ];
801
802 if ( apply_filters( 'timetics/staff/appointment/count_check', false, $meetings_count ) == true ) {
803 return rest_ensure_response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'count_check' ) );
804 }
805
806 $custom_fields = ! empty( $data['custom_fields'] ) ? $data['custom_fields'] : [];
807
808 if ( apply_filters( 'timetics/staff/appointment/custom_field_check', false, $custom_fields ) == true ) {
809 return rest_ensure_response( apply_filters( 'timetics/admin/appointment/error_data', $response, 'custom_field_check' ) );
810 }
811
812 $appoinment_id = (int) $request['appointment_id'];
813 $appoint = new Appointment( $appoinment_id );
814
815 if ( ! $appoint->is_appointment() ) {
816 return new WP_HTTP_Response(
817 [
818 'success' => 0,
819 'status_code' => 404,
820 'message' => esc_html__( 'Invalid appointment id.', 'timetics' ),
821 'data' => [],
822 ],
823 404
824 );
825 }
826
827 $appoint->duplicate();
828
829 $item = $this->prepare_item( $appoint );
830
831 $response = [
832 'success' => 1,
833 'status_code' => 201,
834 'message' => esc_html__( 'Successfully duplicated appointment', 'timetics' ),
835 'data' => $item,
836 ];
837
838 return rest_ensure_response( $response );
839 }
840
841 /**
842 * Save appointment
843 *
844 * @param WP_Rest_Request $request
845 * @param integer $id Appointment id
846 *
847 * @return JSON Updated appoitment data
848 */
849 public function save_appointment( $request, $id = 0 ) {
850 $appoint = new Appointment( $id );
851
852 $data = json_decode( $request->get_body(), true );
853
854 $data = apply_filters( 'timetics_meeting_data', $data );
855
856 $name = ! empty( $data['name'] ) ? sanitize_text_field( $data['name'] ) : $appoint->get_name();
857 $type = ! empty( $data['type'] ) ? sanitize_text_field( $data['type'] ) : $appoint->get_type();
858 $description = ! empty( $data['description'] ) ? sanitize_text_field( $data['description'] ) : '';
859 $staff = ! empty( $data['staff'] ) ? array_map( 'intval', $data['staff'] ) : $appoint->get_staff();
860 $locations = ! empty( $data['locations'] ) ? $data['locations'] : $appoint->get_locations();
861 $duration = ! empty( $data['duration'] ) ? sanitize_text_field( $data['duration'] ) : '';
862 $schedule = ! empty( $data['schedule'] ) ? $data['schedule'] : $appoint->get_schedule();
863 $blocked_schedule = ! empty( $data['blocked_schedule'] ) ? $data['blocked_schedule'] : $appoint->get_blocked_schedule();
864 $price = ! empty( $data['price'] ) ? $data['price'] : '';
865 $categories = ! empty( $data['categories'] ) ? $data['categories'] : '';
866 $buffer_time = ! empty( $data['buffer_time'] ) ? $data['buffer_time'] : '';
867 $timezone = ! empty( $data['timezone'] ) ? $data['timezone'] : '';
868 $availability = ! empty( $data['availability'] ) ? $data['availability'] : '';
869 $visibility = ! empty( $data['visibility'] ) ? strtolower( $data['visibility'] ) : 'enabled';
870 $notifications = ! empty( $data['notifications'] ) ? $data['notifications'] : '';
871 $fleunt_crm_webhook = ! empty( $data['fleunt_crm_webhook'] ) ? $data['fleunt_crm_webhook'] : '';
872 $fluent_hook_overwrite = ! empty( $data['fluent_hook_overwrite'] ) ? (bool) $data['fluent_hook_overwrite'] : false;
873 $pabbly_hook_overwrite = ! empty( $data['pabbly_hook_overwrite'] ) ? (bool) $data['pabbly_hook_overwrite'] : false;
874 $zapier_hook_overwrite = ! empty( $data['zapier_hook_overwrite'] ) ? (bool) $data['zapier_hook_overwrite'] : false;
875 $pabbly_webook = ! empty( $data['pabbly_webook'] ) ? $data['pabbly_webook'] : '';
876 $zapier_webook = ! empty( $data['zapier_webook'] ) ? $data['zapier_webook'] : '';
877 $flowmattic_hook_overwrite = ! empty( $data['flowmattic_hook_overwrite'] ) ? (bool) $data['flowmattic_hook_overwrite'] : false;
878 $flowmattic_webhook = ! empty( $data['flowmattic_webhook'] ) ? esc_url_raw( $data['flowmattic_webhook'] ) : '';
879 $min_notice_time = ! empty( $data['min_notice_time'] ) ? $data['min_notice_time'] : '';
880 $custom_fields = ! empty( $data['custom_fields'] ) ? $data['custom_fields'] : [];
881 $guest_enabled = ! empty( $data['guest_enabled'] ) ? intval( $data['guest_enabled'] ) : false;
882 $guest_limit = ! empty( $data['guest_limit'] ) ? intval( $data['guest_limit'] ) : 1;
883 $capacity = ! empty( $data['capacity'] ) ? intval( $data['capacity'] ) : 1;
884 $appointment_id = ! empty( $data['appointment'] ) ? intval( $data['appointment'] ) : 1;
885 $action = $id ? 'updated' : 'created';
886 $buffer_time_before_value = ! empty( $data['buffer_time_before_value'] ) ? $data['buffer_time_before_value'] : 0;
887 $buffer_time_before_unit = ! empty( $data['buffer_time_before_unit'] ) ? $data['buffer_time_before_unit'] : 'min';
888 $buffer_time_after_value = ! empty( $data['buffer_time_after_value'] ) ? $data['buffer_time_after_value'] : 0;
889 $buffer_time_after_unit = ! empty( $data['buffer_time_after_unit'] ) ? $data['buffer_time_after_unit'] : 'min';
890
891 // Assigned-but-non-owning staff may edit their meeting's schedule/details,
892 // but must not rename it, reassign staff, change visibility, or touch webhook integrations.
893 if ( $id && ! $this->is_appointment_owner( $appoint ) ) {
894 $name = $appoint->get_name();
895 $description = $appoint->get_description();
896 $staff = $appoint->get_staff();
897 $visibility = $appoint->get_visibility();
898 $notifications = $appoint->get_notifications();
899 $fleunt_crm_webhook = $appoint->get_fleunt_crm_webhook();
900 $fluent_hook_overwrite = $appoint->get_fluent_hook_overwrite();
901 $pabbly_hook_overwrite = $appoint->get_pabbly_hook_overwrite();
902 $zapier_hook_overwrite = $appoint->get_zapier_hook_overwrite();
903 $pabbly_webook = $appoint->get_pabbly_webook();
904 $zapier_webook = $appoint->get_zapier_webook();
905 }
906
907 if ( $id ) {
908 $dulicate = $appoint->get_duplicate_nuber();
909 if ( $dulicate && strpos( $name, '-Duplicate' ) == 0 ) {
910 $appoint->update([
911 'duplicate' => 0
912 ]);
913 }
914 }
915
916 if ( is_array( $price ) ) {
917 $ticket_quantity = 0;
918 foreach ( $price as &$ticket ) {
919 if ( empty( $ticket['ticket_price'] ) ) {
920 $ticket['ticket_price'] = 0;
921 }
922 if ( ! empty( $ticket['ticket_quantity'] ) ) {
923 $ticket_quantity += intval( $ticket['ticket_quantity'] );
924 }
925 }
926
927 $capacity = $ticket_quantity;
928 }
929
930 $validate_data = [
931 'name' => $name,
932 'type' => $type,
933 'locations' => $locations,
934 'schedule' => $schedule,
935 'blocked_schedule' => $blocked_schedule,
936 'staff' => $staff,
937
938 ];
939 // Validate input data.
940 $validate = $this->validate( $validate_data, [
941 'name',
942 'type',
943 'locations',
944 'schedule',
945 'staff',
946 ] );
947
948 if ( ! timetics_is_valid_timezone( $timezone ) ) {
949 return new WP_Error( 'timezone_error', __( 'Your timezone is invaid.', 'timetics' ) );
950 }
951
952 $lolcation_errors = $this->get_location_errors( $locations, $staff );
953
954 if ( $lolcation_errors ) {
955 $data = [
956 'status_code' => 409,
957 'success' => 0,
958 'message' => $lolcation_errors,
959 'data' => [],
960 ];
961
962 return new WP_HTTP_Response( $data, 409 );
963 }
964
965 if ( is_wp_error( $validate ) ) {
966 $data = [
967 'status_code' => 409,
968 'success' => 0,
969 'message' => $validate->get_error_messages(),
970 'data' => [],
971 ];
972
973 return new WP_HTTP_Response( $data, 409 );
974 }
975
976 // Save appointment.
977 $appointment_data = [
978 'name' => str_replace( '-Duplicate', '', $name ),
979 'description' => $description,
980 'type' => $type,
981 'locations' => $locations,
982 'staff' => $staff,
983 'duration' => $duration,
984 'price' => $price,
985 'capacity' => $capacity,
986 'schedule' => $schedule,
987 'blocked_schedule' => $blocked_schedule,
988 'categories' => $categories,
989 'timezone' => $timezone,
990 'availability' => $availability,
991 'visibility' => $visibility,
992 'buffer_time' => $buffer_time,
993 'notifications' => $notifications,
994 'fleunt_crm_webhook' => $fleunt_crm_webhook,
995 'fluent_hook_overwrite' => $fluent_hook_overwrite,
996 'pabbly_hook_overwrite' => $pabbly_hook_overwrite,
997 'zapier_hook_overwrite' => $zapier_hook_overwrite,
998 'pabbly_webook' => $pabbly_webook,
999 'zapier_webook' => $zapier_webook,
1000 'flowmattic_hook_overwrite' => $flowmattic_hook_overwrite,
1001 'flowmattic_webhook' => $flowmattic_webhook,
1002 'min_notice_time' => $min_notice_time,
1003 'custom_fields' => $custom_fields,
1004 'guest_enabled' => $guest_enabled,
1005 'guest_limit' => $guest_limit,
1006 'buffer_time_before_value' => $buffer_time_before_value,
1007 'buffer_time_before_unit' => $buffer_time_before_unit,
1008 'buffer_time_after_value' => $buffer_time_after_value,
1009 'buffer_time_after_unit' => $buffer_time_after_unit,
1010
1011 ];
1012
1013 // The sanitised array is the filterable value; $data (raw body) is only
1014 // a reference arg. Getting this order backwards silently discards every
1015 // sanitizer/intval() above and lets the caller write arbitrary post meta.
1016 $appointment_data = apply_filters( 'timetics_meeting_insert_data', $appointment_data, $data );
1017
1018 $appoint->set_props( $appointment_data );
1019 $appoint->save();
1020
1021 // Assign meeting category.
1022 wp_set_post_terms( $appoint->get_id(), $categories, 'timetics-meeting-category' );
1023
1024 do_action( 'timetics_meeting_after_insert', $appoint, $data );
1025
1026 // Prepare response data.
1027 $item = $this->prepare_item( $appoint );
1028
1029 $response = [
1030 'status_code' => 201,
1031 'success' => 1,
1032 /* translators: %s: Action performed (created, updated, etc.) */
1033 'message' => sprintf( esc_html__( 'Successfully %s meeting', 'timetics' ), $action ),
1034 'data' => $item,
1035 ];
1036
1037 return rest_ensure_response( $response );
1038 }
1039
1040 /**
1041 * Prepare item for response
1042 *
1043 * @param integer $appoinment_id
1044 *
1045 * @return array
1046 */
1047 public function prepare_item( $appoint_id, $timezone = '' ) {
1048 $appointment = new Appointment( $appoint_id );
1049 $dulicate = $appointment->get_duplicate_nuber();
1050
1051 $dulicate_text = $dulicate ? ' -Duplicate' : '';
1052 $custom_fields = $appointment->get_custom_fields();
1053 $data = [
1054 'id' => $appointment->get_id(),
1055 'name' => $appointment->get_name() . $dulicate_text,
1056 'image' => $appointment->get_image(),
1057 // 'link' => $appointment->get_link(),
1058 'description' => $appointment->get_description(),
1059 'type' => $appointment->get_type(),
1060 'locations' => $appointment->get_locations(),
1061 'schedule' => $appointment->get_schedule(),
1062 'blocked_schedule' => $appointment->get_blocked_schedule(),
1063 'price' => $appointment->get_price(),
1064 'categories' => $appointment->get_category_ids(),
1065 'staff' => $appointment->get_staff(),
1066 'buffer_time' => $appointment->get_buffer_time(),
1067 'timezone' => $appointment->get_timezone(),
1068 'availability' => $appointment->get_availability(),
1069 'visibility' => $appointment->get_visibility(),
1070 'duration' => $appointment->get_duration(),
1071 'notifications' => $appointment->get_notifications(),
1072 'capacity' => $appointment->get_capacity(),
1073 'fluent_hook_overwrite' => $appointment->get_fluent_hook_overwrite(),
1074 'fleunt_crm_webhook' => $appointment->get_fleunt_crm_webhook(),
1075 'pabbly_hook_overwrite' => $appointment->get_pabbly_hook_overwrite(),
1076 'pabbly_webook' => $appointment->get_pabbly_webook(),
1077 'zapier_hook_overwrite' => $appointment->get_zapier_hook_overwrite(),
1078 'zapier_webook' => $appointment->get_zapier_webook(),
1079 'flowmattic_hook_overwrite' => $appointment->get_flowmattic_hook_overwrite(),
1080 'flowmattic_webhook' => $appointment->get_flowmattic_webhook(),
1081 'min_notice_time' => $appointment->get_min_notice_time(),
1082 'custom_fields' => $custom_fields ?: [],
1083 'permalink' => get_permalink( $appointment->get_id() ),
1084 'guest_enabled' => $appointment->get_guest_enabled(),
1085 'guest_limit' => $appointment->get_guest_limit(),
1086 'author' => $appointment->get_author(),
1087 'buffer_time_before_value' => $appointment->get_buffer_time_before_value(),
1088 'buffer_time_before_unit' => $appointment->get_buffer_time_before_unit(),
1089 'buffer_time_after_value' => $appointment->get_buffer_time_after_value(),
1090 'buffer_time_after_unit' => $appointment->get_buffer_time_after_unit(),
1091 ];
1092
1093 // Strip webhook URLs, notifications, and staff PII for non-privileged callers — several read routes here are public.
1094 if ( ! current_user_can( 'edit_meeting' ) ) {
1095 unset(
1096 $data['notifications'],
1097 $data['fluent_hook_overwrite'],
1098 $data['fleunt_crm_webhook'],
1099 $data['pabbly_hook_overwrite'],
1100 $data['pabbly_webook'],
1101 $data['zapier_hook_overwrite'],
1102 $data['zapier_webook'],
1103 $data['author']
1104 );
1105
1106 if ( ! empty( $data['staff'] ) && is_array( $data['staff'] ) ) {
1107 $data['staff'] = array_map(
1108 function ( $staff ) {
1109 return [
1110 'id' => $staff['id'] ?? 0,
1111 'full_name' => $staff['full_name'] ?? '',
1112 'image' => $staff['image'] ?? '',
1113 ];
1114 },
1115 $data['staff']
1116 );
1117 }
1118 }
1119
1120 return apply_filters( 'timetics_meeting_json_data', $data, $appointment );
1121 }
1122
1123 /**
1124 * Validate location with zoom and google connection
1125 *
1126 * @param array $locations
1127 * @param array $staffs
1128 *
1129 * @return array
1130 */
1131 public function get_location_errors( $locations, $staffs ) {
1132
1133 $errors = [];
1134
1135 foreach ( $staffs as $staff_id ) {
1136 $staff = new Staff( $staff_id );
1137 foreach ( $locations as $location ) {
1138 switch ( $location['location_type'] ) {
1139 case 'google-meet':
1140 if ( ! timetics_is_google_meet_connected( $staff_id ) ) {
1141 $errors[] = sprintf( '%s %s', $staff->get_display_name(), esc_html__( 'is not connected to google meet. Please connect to google meet then try again', 'timetics' ) );
1142 }
1143 break;
1144 case 'zoom':
1145 // Check if zoom addon plugin is active
1146 if ( ! is_plugin_active( 'timetics-zoom-addon/timetics-zoom-addon.php' ) ) {
1147 $errors[] = sprintf( '%s %s', $staff->get_display_name(), esc_html__( 'Zoom addon plugin is not active. Please activate the plugin then try again', 'timetics' ) );
1148 break;
1149 }
1150
1151 // if zoom_connection_type is server_to_server then no need to check for zoom connection
1152 if ( timetics_get_option( 'zoom_connection_type' ) == 'server_to_server' ) {
1153 break;
1154 }
1155
1156 if ( ! timetics_is_zoom_connected( $staff_id ) ) {
1157 $errors[] = sprintf( '%s %s', $staff->get_display_name(), esc_html__( 'is not connected to zoom. Please connect to zoom then try again', 'timetics' ) );
1158 }
1159 break;
1160 }
1161 }
1162 }
1163
1164 return $errors;
1165 }
1166 }
1167