PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.64
Timetics – Appointment Booking Calendar & Scheduling v1.0.64
1.0.64 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 All 65 releases
← All changes | core/appointments/appointment.php +545 -66 1.0.1 → 1.0.64 View file →
@@ -7,8 +7,12 @@
7 7 * @package Timetics
8 8 */
9 9 namespace Timetics\Core\Appointments;
10 10
11 +defined( 'ABSPATH' ) || exit;
12 +
13 +use DateTime;
14 +use Timetics\Base\PostModel;
11 15 use Timetics\Core\Bookings\Booking_Entry;
12 16 use Timetics\Core\Staffs\Staff;
13 17 use WP_Query;
14 18
@@ -16,9 +20,9 @@
16 20 * Class Appointments.
17 21 *
18 22 * @since 1.0.0
19 23 */
20 -class Appointment {
24 +class Appointment extends PostModel {
21 25 /**
22 26 * Store post type
23 27 *
24 28 * @since 1.0.0
@@ -24,9 +28,9 @@
24 28 * @since 1.0.0
25 29 *
26 30 * @var string $post_type
27 31 */
28 - protected static $post_type = 'timetics-appointment';
32 + public $post_type = 'timetics-appointment';
29 33
30 34 /**
31 35 * Store timetics custom taxonomy
32 36 *
@@ -33,9 +37,9 @@
33 37 * @since 1.0.0
34 38 *
35 39 * @var string $taxonomy
36 40 */
37 - protected $taxonomy = 'tt_category';
41 + protected $taxonomy = 'timetics-meeting-category';
38 42
39 43 /**
40 44 * Store id
41 45 *
@@ -51,24 +55,42 @@
51 55 * @since 1.0.0
52 56 *
53 57 * @var array $data
54 58 */
55 - protected $data = [
56 - 'name' => '',
57 - 'type' => '',
58 - 'description' => '',
59 - 'staff' => '',
60 - 'locations' => '',
61 - 'duration' => '',
62 - 'schedule' => '',
63 - 'price' => '',
64 - 'buffer_time' => '',
65 - 'timezone' => '',
66 - 'availability' => '',
67 - 'visibility' => '',
68 - 'capacity' => '',
69 - 'duplicate' => 0,
70 - 'notifications' => [
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' => [
71 93 'booking_created_email_form' => '',
72 94 'booking_created_email_title' => '',
73 95 'booking_created_email_body' => '',
74 96 'booking_canceled_email_form' => '',
@@ -81,8 +103,15 @@
81 103 'booking_reminder_email_title' => '',
82 104 'booking_reminder_email_body' => '',
83 105 'booking_reminder_time' => 30,
84 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,
85 114 ];
86 115
87 116 /**
88 117 * Store meta key prefix
@@ -88,11 +117,11 @@
88 117 * Store meta key prefix
89 118 *
90 119 * @since 1.0.0
91 120 *
92 - * @var string $meta_prefix
121 + * @var string $prefix
93 122 */
94 - protected $meta_prefix = '_tt_apointment_';
123 + public $prefix = '_tt_apointment_';
95 124
96 125 /**
97 126 * Appointments Constructor
98 127 *
@@ -122,8 +151,32 @@
122 151 return $this->id;
123 152 }
124 153
125 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 + /**
126 179 * Get name.
127 180 *
128 181 * @since 1.0.0
129 182 *
@@ -163,9 +216,11 @@
163 216 *
164 217 * @return array
165 218 */
166 219 public function get_staff_ids() {
167 - return $this->get_prop( 'staff' );
220 + $ids = $this->get_prop( 'staff' );
221 +
222 + return is_array( $ids ) ? array_values( $ids ) : [];
168 223 }
169 224
170 225 /**
171 226 * Get duplicate number
@@ -172,9 +227,9 @@
172 227 *
173 228 * @return integer
174 229 */
175 230 public function get_duplicate_nuber() {
176 - return $this->get_prop('duplicate');
231 + return $this->get_prop( 'duplicate' );
177 232 }
178 233
179 234 /**
180 235 * Get capacity
@@ -185,8 +240,25 @@
185 240 return $this->get_prop( 'capacity' );
186 241 }
187 242
188 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 + /**
189 261 * Get location
190 262 *
191 263 * @since 1.0.0
192 264 *
@@ -196,8 +268,19 @@
196 268 return $this->get_prop( 'locations' );
197 269 }
198 270
199 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 + /**
200 283 * Get duration
201 284 *
202 285 * @since 1.0.0
203 286 *
@@ -214,10 +297,10 @@
214 297 */
215 298 public function get_interval() {
216 299 $duration = $this->get_duration();
217 300 if ( $duration ) {
218 - $duration = explode(' ', $duration);
219 - $duration = 'hr' === $duration[1] ? intval($duration[0]) * 60 * 60 : intval($duration[0]) * 60;
301 + $duration = explode( ' ', $duration );
302 + $duration = 'hr' === $duration[1] ? intval( $duration[0] ) * 60 * 60 : intval( $duration[0] ) * 60;
220 303 }
221 304
222 305 return $duration;
223 306 }
@@ -233,8 +316,19 @@
233 316 return $this->get_prop( 'schedule' );
234 317 }
235 318
236 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 + /**
237 331 * Get price
238 332 *
239 333 * @since 1.0.0
240 334 *
@@ -269,8 +363,44 @@
269 363 public function get_buffer_time() {
270 364 return $this->get_prop( 'buffer_time' );
271 365 }
272 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 +
273 403 public function get_timezone() {
274 404 return $this->get_prop( 'timezone' );
275 405 }
276 406
@@ -280,10 +410,52 @@
280 410
281 411 public function get_visibility() {
282 412 return $this->get_prop( 'visibility' );
283 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 + }
284 447
285 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 + /**
286 458 * Get notifications
287 459 *
288 460 * @return array
289 461 */
@@ -291,8 +463,38 @@
291 463 return $this->get_prop( 'notifications' );
292 464 }
293 465
294 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 + /**
295 497 * Get appointment categories
296 498 *
297 499 * @since 1.0.0
298 500 *
@@ -308,8 +510,25 @@
308 510 return $categories;
309 511 }
310 512
311 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 + /**
312 531 * Get post thumbnail.
313 532 *
314 533 * @param string $size Image size
315 534 *
@@ -328,8 +547,75 @@
328 547 return get_post_permalink( $this->id );
329 548 }
330 549
331 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 + /**
332 618 * Get appointment data
333 619 *
334 620 * @param string $prop
335 621 *
@@ -334,9 +620,9 @@
334 620 * @param string $prop
335 621 *
336 622 * @return mixed
337 623 */
338 - private function get_prop( $prop = '' ) {
624 + public function get_prop( $prop = '' ) {
339 625 return $this->get_metadata( $prop );
340 626 }
341 627
342 628 /**
@@ -348,9 +634,9 @@
348 634 *
349 635 * @return mixed
350 636 */
351 637 private function get_metadata( $prop = '' ) {
352 - $meta_key = $this->meta_prefix . $prop;
638 + $meta_key = $this->prefix . $prop;
353 639 return get_post_meta( $this->id, $meta_key, true );
354 640 }
355 641
356 642 /**
@@ -373,9 +659,9 @@
373 659 *
374 660 * @return void
375 661 */
376 662 public function set_props( $data = [] ) {
377 - $this->data = wp_parse_args( $data, $this->data );
663 + $this->data = $data;
378 664 }
379 665
380 666 /**
381 667 * Save appointment
@@ -387,15 +673,17 @@
387 673 public function save() {
388 674
389 675 $args = [
390 676 'post_title' => $this->data['name'],
391 - 'post_type' => self::$post_type,
677 + 'post_type' => $this->post_type,
392 678 'post_status' => 'publish',
393 - 'post_author' => 1,
394 679 ];
395 680
396 681 if ( ! empty( $this->id ) ) {
397 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();
398 686 }
399 687
400 688 // Insert or Update appointment.
401 689 $appoint_id = wp_insert_post( $args );
@@ -415,9 +703,9 @@
415 703 */
416 704 private function save_metadata() {
417 705 foreach ( $this->data as $key => $value ) {
418 706 // Prepare meta key.
419 - $meta_key = $this->meta_prefix . $key;
707 + $meta_key = $this->prefix . $key;
420 708
421 709 // Update appointment meta data.
422 710 update_post_meta( $this->id, $meta_key, $value );
423 711 }
@@ -423,8 +711,30 @@
423 711 }
424 712 }
425 713
426 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 + /**
427 737 * Delete appointment
428 738 *
429 739 * @return bool | WP_Error
430 740 */
@@ -439,9 +749,9 @@
439 749 */
440 750 public function is_appointment() {
441 751 $post = get_post( $this->id );
442 752
443 - if ( $post && self::$post_type === $post->post_type ) {
753 + if ( $post && $this->post_type === $post->post_type ) {
444 754 return true;
445 755 }
446 756
447 757 return false;
@@ -455,9 +765,9 @@
455 765 * @return array
456 766 */
457 767 public static function all( $args = [] ) {
458 768 $defaults = [
459 - 'post_type' => self::$post_type,
769 + 'post_type' => ( new self )->post_type,
460 770 'post_status' => 'publish',
461 771 'posts_per_page' => 20,
462 772 'paged' => 1,
463 773 'orderby' => 'ID',
@@ -465,9 +775,10 @@
465 775 ];
466 776
467 777 $args = wp_parse_args( $args, $defaults );
468 778
469 - if ( ! empty( $args['staff'] ) ) {
779 + // isset()/'' check, not empty() — empty(0) skipped this filter, leaking all staff data to guests.
780 + if ( isset( $args['staff'] ) && '' !== $args['staff'] ) {
470 781 $args['meta_query'][] = [
471 782 'key' => '_tt_apointment_staff',
472 783 'value' => $args['staff'],
473 784 'compare' => 'LIKE',
@@ -481,8 +792,24 @@
481 792 'compare' => 'LIKE',
482 793 ];
483 794 }
484 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 +
485 812 $post = new WP_Query( $args );
486 813
487 814 return [
488 815 'total' => $post->found_posts,
@@ -498,24 +825,35 @@
498 825 * @return void
499 826 */
500 827 public function duplicate() {
501 828 $this->set_props( [
502 - 'name' => $this->get_name(),
503 - 'description' => $this->get_description(),
504 - 'type' => $this->get_type(),
505 - 'locations' => $this->get_locations(),
506 - 'staff' => $this->get_staff_ids(),
507 - 'duplicate' => intval( $this->get_duplicate_nuber() ) + 1,
508 - 'duration' => $this->get_duration(),
509 - 'price' => $this->get_price(),
510 - 'capacity' => $this->get_capacity(),
511 - 'schedule' => $this->get_schedule(),
512 - 'categories' => $this->get_categories(),
513 - 'timezone' => $this->get_timezone(),
514 - 'availability' => $this->get_availability(),
515 - 'visibility' => $this->get_visibility(),
516 - 'buffer_time' => $this->get_buffer_time(),
517 - 'notifications' => $this->get_notifications(),
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(),
518 856 ] );
519 857
520 858 $this->set_id( 0 );
521 859 $this->save();
@@ -529,9 +867,9 @@
529 867 * @param integer $staff_id
530 868 *
531 869 * @return array
532 870 */
533 - public function prepare_schedule( $start, $end, $staff_id ) {
871 + public function prepare_schedule( $start, $end, $staff_id, $timze_zone = '' ) {
534 872 $start = new \DateTime( $start );
535 873 $end = new \DateTime( $end );
536 874 $schedule = [];
537 875
@@ -536,9 +874,9 @@
536 874 $schedule = [];
537 875
538 876 // Prepare schedule for the current meeting
539 877 for ( $date = $start; $date <= $end; $date->modify( '+1 day' ) ) {
540 - $schedule[] = $this->get_schedule_by_date( $date->format( 'Y-m-d' ), $staff_id );
878 + $schedule[] = $this->get_schedule_by_date( $date->format( 'Y-m-d' ), $staff_id, $timze_zone );
541 879 }
542 880
543 881 return $schedule;
544 882 }
@@ -547,31 +885,42 @@
547 885 * Get meeting schedule by date
548 886 *
549 887 * @param string $date [$date description]
550 888 * @param integer $staff_id [$staff_id description]
889 + * @param string $timze_zone [$timze_zone description]
551 890 *
552 891 * @return array
553 892 */
554 - private function get_schedule_by_date( $date, $staff_id ) {
555 - $day_name = ucfirst( date( 'D', strtotime( $date ) ) );
556 - $schedule = $this->get_schedule();
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();
557 896 $interval = $this->get_interval();
558 - $tartget_schedule = [];
897 + $target_schedule = [];
559 898
560 899 $slots = [];
561 900
562 901 if ( ! empty( $schedule[$staff_id] ) ) {
563 - $tartget_schedule = $schedule[$staff_id][$day_name];
902 + $target_schedule = $schedule[$staff_id][$day_name];
564 903 }
565 904
566 - foreach ( $tartget_schedule as $day ) {
905 + foreach ( $target_schedule as $day ) {
567 906 $start = strtotime( $day['start'] );
568 907 $end = strtotime( $day['end'] );
569 908
570 - for ( $time = $start; $time <= $end; $time += $interval ) {
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 ) {
571 920 $booked_entry = $this->get_booking_entries( $date, $time, $staff_id );
572 921 $status = 'available';
573 - $capacity = $this->get_capacity();
922 + $capacity = $this->get_effective_capacity();
574 923 $booked = 0;
575 924
576 925 if ( $booked_entry ) {
577 926 if ( $booked_entry->get_booked() >= $capacity ) {
@@ -580,14 +929,20 @@
580 929
581 930 $booked = $booked_entry->get_booked();
582 931 }
583 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 +
584 937 $slot = [
585 938 'status' => $status,
586 - 'start_time' => gmdate( 'g:ia', $time ),
939 + 'start_time' => $datetime->format( 'g:ia' ),
587 940 'booked' => $booked,
588 941 ];
589 942
943 + $slot = apply_filters( 'timetics_booking_slot', $slot, $this, $booked_entry );
944 +
590 945 $slots[] = $slot;
591 946 }
592 947 }
593 948
@@ -607,21 +962,145 @@
607 962 *
608 963 * @return bool
609 964 */
610 965 private function get_booking_entries( $date, $time, $staff_id ) {
611 - $time = gmdate( 'g:ia', $time );
966 + $time = gmdate( 'h:i a', $time );
612 967 $booking_entries = new Booking_Entry();
613 968 $entries = $booking_entries->find( [
614 - 'meeting_id' => $this->id,
615 969 'staff_id' => $staff_id,
616 - 'date' => $date,
970 + 'date' => $this->convert_timezone( $date, $this->get_timezone())->format('Y-m-d'),
617 971 ] );
618 972
619 973 foreach ( $entries as $entry ) {
620 - if ( $entry->get_start() == $time ) {
621 - return $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;
622 980 }
981 +
982 + if ( $this->is_orphan_entry( $entry ) ) {
983 + continue;
984 + }
985 +
986 + return $entry;
623 987 }
624 988
625 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;
626 1105 }
627 1106 }