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 / appointment.php

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

1,107 lines 28.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Apointment
4 *
5 * @since 1.0.0
6 *
7 * @package Timetics
8 */
9 namespace Timetics\Core\Appointments;
10
11 defined( 'ABSPATH' ) || exit;
12
13 use DateTime;
14 use Timetics\Base\PostModel;
15 use Timetics\Core\Bookings\Booking_Entry;
16 use Timetics\Core\Staffs\Staff;
17 use WP_Query;
18
19 /**
20 * Class Appointments.
21 *
22 * @since 1.0.0
23 */
24 class Appointment extends PostModel {
25 /**
26 * Store post type
27 *
28 * @since 1.0.0
29 *
30 * @var string $post_type
31 */
32 public $post_type = 'timetics-appointment';
33
34 /**
35 * Store timetics custom taxonomy
36 *
37 * @since 1.0.0
38 *
39 * @var string $taxonomy
40 */
41 protected $taxonomy = 'timetics-meeting-category';
42
43 /**
44 * Store id
45 *
46 * @since 1.0.0
47 *
48 * @var int $id
49 */
50 protected $id;
51
52 /**
53 * Store post metadata
54 *
55 * @since 1.0.0
56 *
57 * @var array $data
58 */
59 public $data = [
60 'name' => '',
61 'type' => '',
62 'description' => '',
63 'staff' => '',
64 'locations' => '',
65 'duration' => '',
66 'schedule' => '',
67 'blocked_schedule' => '',
68 'price' => '',
69 'buffer_time' => '',
70 'buffer_time_before_value'=> '',
71 'buffer_time_before_unit' => '',
72 'buffer_time_after_value' => '',
73 'buffer_time_after_unit' => '',
74 'timezone' => '',
75 'availability' => '',
76 'visibility' => '',
77 'capacity' => '',
78 'seat_plan' => '',
79 'duplicate' => 0,
80 'custom_fields' => [],
81 'wc_product_id' => 0,
82 'seat_plan_settings' => '',
83 'fleunt_crm_webhook' => '',
84 'fluent_hook_overwrite' => '',
85 'pabbly_hook_overwrite' => '',
86 'zapier_hook_overwrite' => '',
87 'pabbly_webook' => '',
88 'zapier_webook' => '',
89 'flowmattic_hook_overwrite' => '',
90 'flowmattic_webhook' => '',
91 'min_notice_time' => '',
92 'notifications' => [
93 'booking_created_email_form' => '',
94 'booking_created_email_title' => '',
95 'booking_created_email_body' => '',
96 'booking_canceled_email_form' => '',
97 'booking_canceled_email_title' => '',
98 'booking_canceled_email_body' => '',
99 'booking_reschedule_email_form' => '',
100 'booking_reschedule_email_title' => '',
101 'booking_reschedule_email_body' => '',
102 'booking_reminder_email_form' => '',
103 'booking_reminder_email_title' => '',
104 'booking_reminder_email_body' => '',
105 'booking_reminder_time' => 30,
106 ],
107 'permalink' => '',
108 'recurring' => false,
109 'recurring_limit' => 0,
110 'recurring_interval' => 1,
111 'recurring_interval_name' => 'day',
112 'guest_enabled' => false,
113 'guest_limit' => 1,
114 ];
115
116 /**
117 * Store meta key prefix
118 *
119 * @since 1.0.0
120 *
121 * @var string $prefix
122 */
123 public $prefix = '_tt_apointment_';
124
125 /**
126 * Appointments Constructor
127 *
128 * @since 1.0.0
129 *
130 * @param int $appointment Optional. Default 0.
131 * @return void
132 */
133 public function __construct( $appointment = 0 ) {
134 if ( $appointment instanceof self ) {
135 $this->set_id( $appointment->get_id() );
136 } elseif ( ! empty( $appointment->ID ) ) {
137 $this->set_id( $appointment->ID );
138 } elseif ( is_numeric( $appointment ) && $appointment > 0 ) {
139 $this->set_id( $appointment );
140 }
141 }
142
143 /**
144 * Get appoint id.
145 *
146 * @since 1.0.0
147 *
148 * @return int
149 */
150 public function get_id() {
151 return $this->id;
152 }
153
154 /**
155 * Get appointment slug
156 *
157 * @return mixed
158 */
159 public function get_slug() {
160 $post = get_post( $this->get_id() );
161
162 if ( $post ) {
163 return $post->post_name;
164 }
165
166 return false;
167 }
168
169 /**
170 * Get woocommerce product id
171 *
172 * @return integer
173 */
174 public function get_wc_product_id() {
175 return $this->get_prop( 'wc_product_id' );
176 }
177
178 /**
179 * Get name.
180 *
181 * @since 1.0.0
182 *
183 * @return string
184 */
185 public function get_name() {
186 return $this->get_prop( 'name' );
187 }
188
189 /**
190 * Get coach
191 *
192 * @since 1.0.0
193 *
194 * @return int
195 */
196 public function get_staff() {
197 $staff_ids = $this->get_prop( 'staff' );
198 $staffs = [];
199
200 if ( $staff_ids ) {
201 foreach ( $staff_ids as $staff_id ) {
202 $staff = new Staff( $staff_id );
203
204 if ( ! $staff->is_staff() ) {
205 continue;
206 }
207
208 $staffs[] = $staff->get_data();
209 }
210 }
211 return $staffs;
212 }
213
214 /**
215 * Get staff ids
216 *
217 * @return array
218 */
219 public function get_staff_ids() {
220 $ids = $this->get_prop( 'staff' );
221
222 return is_array( $ids ) ? array_values( $ids ) : [];
223 }
224
225 /**
226 * Get duplicate number
227 *
228 * @return integer
229 */
230 public function get_duplicate_nuber() {
231 return $this->get_prop( 'duplicate' );
232 }
233
234 /**
235 * Get capacity
236 *
237 * @return integer
238 */
239 public function get_capacity() {
240 return $this->get_prop( 'capacity' );
241 }
242
243 /**
244 * Get the effective booking capacity used for availability checks.
245 *
246 * One-to-One meetings always allow a single booking per slot regardless of
247 * the stored capacity meta (which is only meaningful for group meetings),
248 * matching the one-to-one handling used elsewhere in the booking flow.
249 *
250 * @return integer
251 */
252 public function get_effective_capacity() {
253 if ( 'one-to-one' === strtolower( (string) $this->get_type() ) ) {
254 return 1;
255 }
256
257 return intval( $this->get_capacity() );
258 }
259
260 /**
261 * Get location
262 *
263 * @since 1.0.0
264 *
265 * @return string
266 */
267 public function get_locations() {
268 return $this->get_prop( 'locations' );
269 }
270
271 /**
272 * Get Permalink
273 *
274 * @since 1.0.0
275 *
276 * @return string
277 */
278 public function get_appointment_permalink() {
279 return get_post_permalink( $this->id );
280 }
281
282 /**
283 * Get duration
284 *
285 * @since 1.0.0
286 *
287 * @return string
288 */
289 public function get_duration() {
290 return $this->get_prop( 'duration' );
291 }
292
293 /**
294 * Get interval for the duration
295 *
296 * @return integer
297 */
298 public function get_interval() {
299 $duration = $this->get_duration();
300 if ( $duration ) {
301 $duration = explode( ' ', $duration );
302 $duration = 'hr' === $duration[1] ? intval( $duration[0] ) * 60 * 60 : intval( $duration[0] ) * 60;
303 }
304
305 return $duration;
306 }
307
308 /**
309 * Get schedule
310 *
311 * @since 1.0.0
312 *
313 * @return array
314 */
315 public function get_schedule() {
316 return $this->get_prop( 'schedule' );
317 }
318
319 /**
320 * Get blocked schedule
321 *
322 * @since 1.0.0
323 *
324 * @return array
325 */
326 public function get_blocked_schedule() {
327 return $this->get_prop( 'blocked_schedule' );
328 }
329
330 /**
331 * Get price
332 *
333 * @since 1.0.0
334 *
335 * @return string
336 */
337 public function get_price() {
338 return $this->get_prop( 'price' );
339 }
340
341 /**
342 * Get type
343 *
344 * @since 1.0.0
345 *
346 * @return string
347 */
348 public function get_type() {
349 return $this->get_prop( 'type' );
350 }
351
352 /**
353 * Get description
354 *
355 * @since 1.0.0
356 *
357 * @return string
358 */
359 public function get_description() {
360 return $this->get_prop( 'description' );
361 }
362
363 public function get_buffer_time() {
364 return $this->get_prop( 'buffer_time' );
365 }
366
367 /**
368 * Returns the amount of buffer time before the appointment (without unit like minute or hour)
369 *
370 * @return integer
371 */
372 public function get_buffer_time_before_value() {
373 return $this->get_prop( 'buffer_time_before_value' );
374 }
375
376 /**
377 * Returns the unit of buffer time before the appointment (like minute or hour)
378 *
379 * @return string
380 */
381 public function get_buffer_time_before_unit() {
382 return $this->get_prop( 'buffer_time_before_unit' );
383 }
384
385 /**
386 * Returns the amount of buffer time after the appointment (without unit like minute or hour)
387 *
388 * @return integer
389 */
390 public function get_buffer_time_after_value() {
391 return $this->get_prop( 'buffer_time_after_value' );
392 }
393
394 /**
395 * Returns the unit of buffer time after the appointment (like minute or hour)
396 *
397 * @return string
398 */
399 public function get_buffer_time_after_unit() {
400 return $this->get_prop( 'buffer_time_after_unit' );
401 }
402
403 public function get_timezone() {
404 return $this->get_prop( 'timezone' );
405 }
406
407 public function get_availability() {
408 return $this->get_prop( 'availability' );
409 }
410
411 public function get_visibility() {
412 return $this->get_prop( 'visibility' );
413 }
414 public function get_fleunt_crm_webhook() {
415 return $this->get_prop( 'fleunt_crm_webhook' );
416 }
417 public function get_fluent_hook_overwrite() {
418 return $this->get_prop( 'fluent_hook_overwrite' );
419 }
420 public function get_pabbly_webook() {
421 return $this->get_prop( 'pabbly_webook' );
422 }
423 public function get_pabbly_hook_overwrite() {
424 return $this->get_prop( 'pabbly_hook_overwrite' );
425 }
426 public function get_zapier_webook() {
427 return $this->get_prop( 'zapier_webook' );
428 }
429 public function get_zapier_hook_overwrite() {
430 return $this->get_prop( 'zapier_hook_overwrite' );
431 }
432 public function get_flowmattic_webhook() {
433 return $this->get_prop( 'flowmattic_webhook' );
434 }
435 public function get_flowmattic_hook_overwrite() {
436 return $this->get_prop( 'flowmattic_hook_overwrite' );
437 }
438 public function get_min_notice_time() {
439 return $this->get_prop( 'min_notice_time' );
440 }
441 public function get_guest_enabled() {
442 return $this->get_prop( 'guest_enabled' );
443 }
444 public function get_guest_limit() {
445 return $this->get_prop( 'guest_limit' );
446 }
447
448 /**
449 * Get Custom fields
450 *
451 * @return array
452 */
453 public function get_custom_fields() {
454 return $this->get_prop( 'custom_fields' );
455 }
456
457 /**
458 * Get notifications
459 *
460 * @return array
461 */
462 public function get_notifications() {
463 return $this->get_prop( 'notifications' );
464 }
465
466 /**
467 * Returns the buffer time before the appointment in seconds
468 *
469 * @return integer seconds
470 */
471 public function get_buffer_time_before_in_seconds() {
472 $buffer_time_value = (int) $this->get_buffer_time_before_value() ?? 0;
473
474 if ( 'hr' === $this->get_buffer_time_before_unit() ) {
475 return $buffer_time_value * 60 * 60;
476 }
477
478 return $buffer_time_value * 60;
479 }
480
481 /**
482 * Returns the buffer time after the appointment in seconds
483 *
484 * @return integer seconds
485 */
486 public function get_buffer_time_after_in_seconds() {
487 $buffer_time_value = (int) $this->get_buffer_time_after_value() ?? 0;
488
489 if ( 'hr' === $this->get_buffer_time_after_unit() ) {
490 return $buffer_time_value * 60 * 60;
491 }
492
493 return $buffer_time_value * 60;
494 }
495
496 /**
497 * Get appointment categories
498 *
499 * @since 1.0.0
500 *
501 * @return array
502 */
503 public function get_categories() {
504 $categories = wp_get_post_terms( $this->id, $this->taxonomy );
505
506 if ( is_wp_error( $categories ) ) {
507 return [];
508 }
509
510 return $categories;
511 }
512
513 /**
514 * Get category ids
515 *
516 * @return array
517 */
518 public function get_category_ids() {
519 $categories = $this->get_categories();
520
521 $ids = [];
522
523 foreach ( $categories as $category ) {
524 $ids[] = $category->term_id;
525 }
526
527 return $ids;
528 }
529
530 /**
531 * Get post thumbnail.
532 *
533 * @param string $size Image size
534 *
535 * @return string
536 */
537 public function get_image( $size = 'post-thumbnail' ) {
538 return get_the_post_thumbnail_url( $size );
539 }
540
541 /**
542 * Get permalink
543 *
544 * @return string
545 */
546 public function get_link() {
547 return get_post_permalink( $this->id );
548 }
549
550 /**
551 * Get Seat plan for a meeting
552 *
553 * @return array
554 */
555 public function get_seats() {
556 return $this->get_prop( 'seat_plan' );
557 }
558
559 /**
560 * Get appointment author
561 *
562 * @return integer
563 */
564 public function get_author() {
565 $post = get_post( $this->id );
566
567 return $post ? (int) $post->post_author : 0;
568 }
569
570 /**
571 * Get seat plan settings
572 *
573 * @return array
574 */
575 public function get_seat_settings() {
576 return $this->get_prop( 'seat_plan_settings' );
577 }
578
579 // Recurring options.
580
581 /**
582 * Check it is recurring or not
583 *
584 * @return bool
585 */
586 public function is_recurring() {
587 return boolval( $this->get_prop( 'recurring' ) );
588 }
589
590 /**
591 * Get recurring limit
592 *
593 * @return integer
594 */
595 public function get_recurring_limit() {
596 return intval( $this->get_prop( 'recurring_limit' ) );
597 }
598
599 /**
600 * Get recurring interval
601 *
602 * @return integer
603 */
604 public function get_recurring_interval() {
605 return intval( $this->get_prop( 'recurring_interval' ) );
606 }
607
608 /**
609 * Get recurring interval name
610 *
611 * @return string
612 */
613 public function get_recurring_interval_name() {
614 return $this->get_prop( 'recurring_interval_name' );
615 }
616
617 /**
618 * Get appointment data
619 *
620 * @param string $prop
621 *
622 * @return mixed
623 */
624 public function get_prop( $prop = '' ) {
625 return $this->get_metadata( $prop );
626 }
627
628 /**
629 * Get metadata
630 *
631 * @since 1.0.0
632 *
633 * @param string $prop Optional. Default empty string.
634 *
635 * @return mixed
636 */
637 private function get_metadata( $prop = '' ) {
638 $meta_key = $this->prefix . $prop;
639 return get_post_meta( $this->id, $meta_key, true );
640 }
641
642 /**
643 * Set appointment id
644 *
645 * @since 1.0.0
646 *
647 * @param int $id
648 *
649 * @return void
650 */
651 public function set_id( $id ) {
652 $this->id = $id;
653 }
654
655 /**
656 * Set props
657 *
658 * @param array $data Metadata array. Default empty array.
659 *
660 * @return void
661 */
662 public function set_props( $data = [] ) {
663 $this->data = $data;
664 }
665
666 /**
667 * Save appointment
668 *
669 * @since 1.0.0
670 *
671 * @return void
672 */
673 public function save() {
674
675 $args = [
676 'post_title' => $this->data['name'],
677 'post_type' => $this->post_type,
678 'post_status' => 'publish',
679 ];
680
681 if ( ! empty( $this->id ) ) {
682 $args['ID'] = $this->id;
683 } else {
684 // Only on creation — setting this on update reassigns post_author to whoever edits it.
685 $args['post_author'] = get_current_user_id();
686 }
687
688 // Insert or Update appointment.
689 $appoint_id = wp_insert_post( $args );
690
691 if ( ! is_wp_error( $appoint_id ) ) {
692 $this->set_id( $appoint_id );
693 $this->save_metadata();
694 }
695 }
696
697 /**
698 * Update appointment meta data.
699 *
700 * @since 1.0.0
701 *
702 * @return void
703 */
704 private function save_metadata() {
705 foreach ( $this->data as $key => $value ) {
706 // Prepare meta key.
707 $meta_key = $this->prefix . $key;
708
709 // Update appointment meta data.
710 update_post_meta( $this->id, $meta_key, $value );
711 }
712 }
713
714 /**
715 * Update meeting data
716 *
717 * @param array $args meeting data
718 *
719 * @return bool
720 */
721 public function update( $args = [] ) {
722
723 foreach ( $args as $key => $value ) {
724 if ( ! array_key_exists( $key, $this->data ) ) {
725 continue;
726 }
727
728 // Prepare meta key.
729 $meta_key = $this->prefix . $key;
730
731 // Update appointment meta data.
732 update_post_meta( $this->id, $meta_key, $value );
733 }
734 }
735
736 /**
737 * Delete appointment
738 *
739 * @return bool | WP_Error
740 */
741 public function delete() {
742 return wp_delete_post( $this->id, true );
743 }
744
745 /**
746 * Check the appoint is valid or not
747 *
748 * @return bool
749 */
750 public function is_appointment() {
751 $post = get_post( $this->id );
752
753 if ( $post && $this->post_type === $post->post_type ) {
754 return true;
755 }
756
757 return false;
758 }
759
760 /**
761 * Get all appointments
762 *
763 * @param array $args Appointments args. Default empty array.
764 *
765 * @return array
766 */
767 public static function all( $args = [] ) {
768 $defaults = [
769 'post_type' => ( new self )->post_type,
770 'post_status' => 'publish',
771 'posts_per_page' => 20,
772 'paged' => 1,
773 'orderby' => 'ID',
774 'order' => 'DESC',
775 ];
776
777 $args = wp_parse_args( $args, $defaults );
778
779 // isset()/'' check, not empty() — empty(0) skipped this filter, leaking all staff data to guests.
780 if ( isset( $args['staff'] ) && '' !== $args['staff'] ) {
781 $args['meta_query'][] = [
782 'key' => '_tt_apointment_staff',
783 'value' => $args['staff'],
784 'compare' => 'LIKE',
785 ];
786 }
787
788 if ( ! empty( $args['visibility'] ) ) {
789 $args['meta_query'][] = [
790 'key' => '_tt_apointment_visibility',
791 'value' => $args['visibility'],
792 'compare' => 'LIKE',
793 ];
794 }
795
796 if ( ! empty( $args['type'] ) ) {
797 $args['meta_query'][] = [
798 'key' => '_tt_apointment_type',
799 'value' => $args['type'],
800 'compare' => 'LIKE',
801 ];
802 }
803
804 if ( ! empty( $args['category'] ) ) {
805 $args['tax_query'][] = [
806 'taxonomy' => 'timetics-meeting-category',
807 'terms' => $args['category'],
808 'include_children' => false,
809 ];
810 }
811
812 $post = new WP_Query( $args );
813
814 return [
815 'total' => $post->found_posts,
816 'items' => $post->posts,
817 ];
818 }
819
820 /**
821 * Duplicate appointment
822 *
823 * @since 1.0.0
824 *
825 * @return void
826 */
827 public function duplicate() {
828 $this->set_props( [
829 'name' => $this->get_name(),
830 'description' => $this->get_description(),
831 'type' => $this->get_type(),
832 'locations' => $this->get_locations(),
833 'staff' => $this->get_staff_ids(),
834 'duplicate' => intval( $this->get_duplicate_nuber() ) + 1,
835 'duration' => $this->get_duration(),
836 'price' => $this->get_price(),
837 'capacity' => $this->get_capacity(),
838 'schedule' => $this->get_schedule(),
839 'categories' => $this->get_categories(),
840 'timezone' => $this->get_timezone(),
841 'availability' => $this->get_availability(),
842 'visibility' => $this->get_visibility(),
843 'buffer_time' => $this->get_buffer_time(),
844 'notifications' => $this->get_notifications(),
845 'fluent_hook_overwrite' => $this->get_fluent_hook_overwrite(),
846 'fleunt_crm_webhook' => $this->get_fleunt_crm_webhook(),
847 'pabbly_hook_overwrite' => $this->get_pabbly_hook_overwrite(),
848 'pabbly_webook' => $this->get_pabbly_webook(),
849 'zapier_webook' => $this->get_zapier_webook(),
850 'flowmattic_webhook' => $this->get_flowmattic_webhook(),
851 'flowmattic_hook_overwrite' => $this->get_flowmattic_hook_overwrite(),
852 'min_notice_time' => $this->get_min_notice_time(),
853 'zapier_hook_overwrite' => $this->get_zapier_hook_overwrite(),
854 'guest_enabled' => $this->get_guest_enabled(),
855 'guest_limit' => $this->get_guest_limit(),
856 ] );
857
858 $this->set_id( 0 );
859 $this->save();
860 }
861
862 /**
863 * Prepare meeting schedule
864 *
865 * @param string $start
866 * @param string $end
867 * @param integer $staff_id
868 *
869 * @return array
870 */
871 public function prepare_schedule( $start, $end, $staff_id, $timze_zone = '' ) {
872 $start = new \DateTime( $start );
873 $end = new \DateTime( $end );
874 $schedule = [];
875
876 // Prepare schedule for the current meeting
877 for ( $date = $start; $date <= $end; $date->modify( '+1 day' ) ) {
878 $schedule[] = $this->get_schedule_by_date( $date->format( 'Y-m-d' ), $staff_id, $timze_zone );
879 }
880
881 return $schedule;
882 }
883
884 /**
885 * Get meeting schedule by date
886 *
887 * @param string $date [$date description]
888 * @param integer $staff_id [$staff_id description]
889 * @param string $timze_zone [$timze_zone description]
890 *
891 * @return array
892 */
893 private function get_schedule_by_date( $date, $staff_id, $time_zone = '' ) {
894 $day_name = ucfirst( gmdate( 'D', strtotime( $date ) ) );
895 $schedule = $this->get_schedule();
896 $interval = $this->get_interval();
897 $target_schedule = [];
898
899 $slots = [];
900
901 if ( ! empty( $schedule[$staff_id] ) ) {
902 $target_schedule = $schedule[$staff_id][$day_name];
903 }
904
905 foreach ( $target_schedule as $day ) {
906 $start = strtotime( $day['start'] );
907 $end = strtotime( $day['end'] );
908
909 // Get buffer times in seconds
910 $buffer_before = $this->get_buffer_time_before_in_seconds();
911 $buffer_after = $this->get_buffer_time_after_in_seconds();
912
913 // Adjust the start time to include buffer before
914 $adjusted_start = $start + $buffer_before;
915
916 // Adjust the end time to ensure we have enough time for the slot + buffer after
917 $min_slot_duration = $interval + $buffer_after;
918
919 for ( $time = $adjusted_start; $time + $min_slot_duration <= $end; $time += $interval + $buffer_after + $buffer_before ) {
920 $booked_entry = $this->get_booking_entries( $date, $time, $staff_id );
921 $status = 'available';
922 $capacity = $this->get_effective_capacity();
923 $booked = 0;
924
925 if ( $booked_entry ) {
926 if ( $booked_entry->get_booked() >= $capacity ) {
927 $status = 'unavailable';
928 }
929
930 $booked = $booked_entry->get_booked();
931 }
932
933 // $time is only a clock reading, so pass the slot's own date or
934 // today's DST offset gets applied to a date in the other season.
935 $datetime = $this->convert_timezone( $date . ' ' . gmdate( 'g:ia', $time ), $time_zone );
936
937 $slot = [
938 'status' => $status,
939 'start_time' => $datetime->format( 'g:ia' ),
940 'booked' => $booked,
941 ];
942
943 $slot = apply_filters( 'timetics_booking_slot', $slot, $this, $booked_entry );
944
945 $slots[] = $slot;
946 }
947 }
948
949 return [
950 'date' => $date,
951 'status' => empty( $slots ) ? 'unavailable' : 'available',
952 'slots' => $slots,
953 ];
954 }
955
956 /**
957 * Get booked entry status
958 *
959 * @param string $date
960 * @param string $time
961 * @param interger $staff_id
962 *
963 * @return bool
964 */
965 private function get_booking_entries( $date, $time, $staff_id ) {
966 $time = gmdate( 'h:i a', $time );
967 $booking_entries = new Booking_Entry();
968 $entries = $booking_entries->find( [
969 'staff_id' => $staff_id,
970 'date' => $this->convert_timezone( $date, $this->get_timezone())->format('Y-m-d'),
971 ] );
972
973 foreach ( $entries as $entry ) {
974 $start_time = $entry->get_start();
975 $end_time = $entry->get_end();
976
977 // Check if current time slot falls within the booking duration
978 if ( ! $this->is_time_in_range( $time, $start_time, $end_time ) ) {
979 continue;
980 }
981
982 if ( $this->is_orphan_entry( $entry ) ) {
983 continue;
984 }
985
986 return $entry;
987 }
988
989 return false;
990 }
991
992 /**
993 * Whether an entry outlived its booking and should stop blocking the slot.
994 *
995 * Only single-booking entries qualify: a shared entry names just its first
996 * booker in `booking_id`, and entries with none belong to events/imports.
997 *
998 * @param Booking_Entry $entry
999 *
1000 * @return bool
1001 */
1002 private function is_orphan_entry( $entry ) {
1003 $booking_id = $entry->get_booking_id();
1004
1005 if ( ! $booking_id || intval( $entry->get_booked() ) > 1 ) {
1006 return false;
1007 }
1008
1009 $booking = get_post( $booking_id );
1010
1011 return ! $booking || 'timetics-booking' !== $booking->post_type;
1012 }
1013
1014 /**
1015 * Check if a time falls within a given time range
1016 *
1017 * @param string $current_time Current time in h:i a format
1018 * @param string $start_time Start time in h:i a format
1019 * @param string $end_time End time in h:i a format
1020 *
1021 * @return bool
1022 */
1023 private function is_time_in_range( $current_time, $start_time, $end_time ) {
1024 $current = strtotime( $current_time );
1025 $start = strtotime( $start_time );
1026 $end = strtotime( $end_time );
1027
1028 return $current >= $start && $current < $end;
1029 }
1030
1031 /**
1032 * Convert timestamp in target timezone
1033 *
1034 * @param integer $timestamp
1035 * @param string $timze_zone
1036 *
1037 * @return DateTime
1038 */
1039 public function convert_timezone( $date, $timze_zone ) {
1040 $date = is_int( $date ) ? gmdate( 'Y-m-d g:ia', $date ) : $date;
1041
1042 // Create a DateTime object with the provided timestamp and time zone
1043 $datetime = new \DateTime( $date, new \DateTimeZone( $this->get_timezone() ) );
1044
1045 // Convert the time zone
1046 $datetime->setTimezone( new \DateTimeZone( $timze_zone ) );
1047
1048 // Get the converted timestamp
1049 return $datetime;
1050 }
1051
1052 /**
1053 * Get appointmentt ids by author id
1054 *
1055 * @param integer $author_id Appointment author id
1056 *
1057 * @return array Appointment ids
1058 */
1059 public static function get_appointment_ids_by_author( $author_id ) {
1060 $query = new WP_Query([
1061 'post_type' => 'timetics-appointment',
1062 'post_status' => 'any',
1063 'author' => $author_id,
1064 'posts_per_page' => -1,
1065 'fields' => 'ids'
1066 ]);
1067
1068 return $query->posts;
1069 }
1070
1071 /**
1072 * Get meeting available time slot
1073 *
1074 * @param string $date Start date
1075 * @param integer $staff_id Meeting assigned staff id
1076 * @param string $timze_zone Booking timezone
1077 *
1078 * @return array Available timeslots
1079 */
1080 public function get_avilable_timeslots( $date, $staff_id, $time_zone = '' ) {
1081 $day_name = ucfirst( gmdate( 'D', strtotime( $date ) ) );
1082 $schedule = $this->get_schedule();
1083 $interval = $this->get_interval();
1084 $tartget_schedule = [];
1085
1086 $slots = [];
1087
1088 if ( isset($schedule[$staff_id]) && ! empty(array_filter($schedule[$staff_id])) ) {
1089 $tartget_schedule = $schedule[$staff_id][$day_name];
1090 }
1091
1092 foreach ( $tartget_schedule as $day ) {
1093 $start = strtotime( $day['start'] );
1094 $end = strtotime( $day['end'] );
1095
1096 for ( $time = $start; $time <= $end; $time += $interval ) {
1097 // Slot's own date, see get_schedule_by_date().
1098 $datetime = $this->convert_timezone( $date . ' ' . gmdate( 'g:ia', $time ), $time_zone );
1099 $slot = $datetime->format( 'g:ia' );
1100 $slots[] = $slot;
1101 }
1102 }
1103
1104 return $slots;
1105 }
1106 }
1107