PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.62
Timetics – Appointment Booking Calendar & Scheduling v1.0.62
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.62, at core/appointments/appointment.php

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