| @@ -7,11 +7,12 @@ | ||
| 7 | 7 | * @package Timetics |
| 8 | 8 | */ |
| 9 | 9 | namespace Timetics\Core\Appointments; |
| 10 | 10 | |
| 11 | +defined( 'ABSPATH' ) || exit; | |
| 12 | + | |
| 11 | 13 | use DateTime; |
| 12 | 14 | use Timetics\Base\PostModel; |
| 13 | -use Timetics\Core\Bookings\Booking; | |
| 14 | 15 | use Timetics\Core\Bookings\Booking_Entry; |
| 15 | 16 | use Timetics\Core\Staffs\Staff; |
| 16 | 17 | use WP_Query; |
| 17 | 18 | |
| @@ -65,8 +66,12 @@ | ||
| 65 | 66 | 'schedule' => '', |
| 66 | 67 | 'blocked_schedule' => '', |
| 67 | 68 | 'price' => '', |
| 68 | 69 | 'buffer_time' => '', |
| 70 | + 'buffer_time_before_value'=> '', | |
| 71 | + 'buffer_time_before_unit' => '', | |
| 72 | + 'buffer_time_after_value' => '', | |
| 73 | + 'buffer_time_after_unit' => '', | |
| 69 | 74 | 'timezone' => '', |
| 70 | 75 | 'availability' => '', |
| 71 | 76 | 'visibility' => '', |
| 72 | 77 | 'capacity' => '', |
| @@ -80,8 +85,10 @@ | ||
| 80 | 85 | 'pabbly_hook_overwrite' => '', |
| 81 | 86 | 'zapier_hook_overwrite' => '', |
| 82 | 87 | 'pabbly_webook' => '', |
| 83 | 88 | 'zapier_webook' => '', |
| 89 | + 'flowmattic_hook_overwrite' => '', | |
| 90 | + 'flowmattic_webhook' => '', | |
| 84 | 91 | 'min_notice_time' => '', |
| 85 | 92 | 'notifications' => [ |
| 86 | 93 | 'booking_created_email_form' => '', |
| 87 | 94 | 'booking_created_email_title' => '', |
| @@ -101,8 +108,10 @@ | ||
| 101 | 108 | 'recurring' => false, |
| 102 | 109 | 'recurring_limit' => 0, |
| 103 | 110 | 'recurring_interval' => 1, |
| 104 | 111 | 'recurring_interval_name' => 'day', |
| 112 | + 'guest_enabled' => false, | |
| 113 | + 'guest_limit' => 1, | |
| 105 | 114 | ]; |
| 106 | 115 | |
| 107 | 116 | /** |
| 108 | 117 | * Store meta key prefix |
| @@ -142,8 +151,23 @@ | ||
| 142 | 151 | return $this->id; |
| 143 | 152 | } |
| 144 | 153 | |
| 145 | 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 | + /** | |
| 146 | 170 | * Get woocommerce product id |
| 147 | 171 | * |
| 148 | 172 | * @return integer |
| 149 | 173 | */ |
| @@ -192,9 +216,11 @@ | ||
| 192 | 216 | * |
| 193 | 217 | * @return array |
| 194 | 218 | */ |
| 195 | 219 | public function get_staff_ids() { |
| 196 | - return $this->get_prop( 'staff' ); | |
| 220 | + $ids = $this->get_prop( 'staff' ); | |
| 221 | + | |
| 222 | + return is_array( $ids ) ? array_values( $ids ) : []; | |
| 197 | 223 | } |
| 198 | 224 | |
| 199 | 225 | /** |
| 200 | 226 | * Get duplicate number |
| @@ -214,8 +240,25 @@ | ||
| 214 | 240 | return $this->get_prop( 'capacity' ); |
| 215 | 241 | } |
| 216 | 242 | |
| 217 | 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 | + /** | |
| 218 | 261 | * Get location |
| 219 | 262 | * |
| 220 | 263 | * @since 1.0.0 |
| 221 | 264 | * |
| @@ -320,8 +363,44 @@ | ||
| 320 | 363 | public function get_buffer_time() { |
| 321 | 364 | return $this->get_prop( 'buffer_time' ); |
| 322 | 365 | } |
| 323 | 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 | + | |
| 324 | 403 | public function get_timezone() { |
| 325 | 404 | return $this->get_prop( 'timezone' ); |
| 326 | 405 | } |
| 327 | 406 | |
| @@ -331,9 +410,8 @@ | ||
| 331 | 410 | |
| 332 | 411 | public function get_visibility() { |
| 333 | 412 | return $this->get_prop( 'visibility' ); |
| 334 | 413 | } |
| 335 | - | |
| 336 | 414 | public function get_fleunt_crm_webhook() { |
| 337 | 415 | return $this->get_prop( 'fleunt_crm_webhook' ); |
| 338 | 416 | } |
| 339 | 417 | public function get_fluent_hook_overwrite() { |
| @@ -350,11 +428,23 @@ | ||
| 350 | 428 | } |
| 351 | 429 | public function get_zapier_hook_overwrite() { |
| 352 | 430 | return $this->get_prop( 'zapier_hook_overwrite' ); |
| 353 | 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 | + } | |
| 354 | 438 | public function get_min_notice_time() { |
| 355 | 439 | return $this->get_prop( 'min_notice_time' ); |
| 356 | 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 | + } | |
| 357 | 447 | |
| 358 | 448 | /** |
| 359 | 449 | * Get Custom fields |
| 360 | 450 | * |
| @@ -373,8 +463,38 @@ | ||
| 373 | 463 | return $this->get_prop( 'notifications' ); |
| 374 | 464 | } |
| 375 | 465 | |
| 376 | 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 | + /** | |
| 377 | 497 | * Get appointment categories |
| 378 | 498 | * |
| 379 | 499 | * @since 1.0.0 |
| 380 | 500 | * |
| @@ -436,8 +556,19 @@ | ||
| 436 | 556 | return $this->get_prop( 'seat_plan' ); |
| 437 | 557 | } |
| 438 | 558 | |
| 439 | 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 | + /** | |
| 440 | 571 | * Get seat plan settings |
| 441 | 572 | * |
| 442 | 573 | * @return array |
| 443 | 574 | */ |
| @@ -544,13 +675,15 @@ | ||
| 544 | 675 | $args = [ |
| 545 | 676 | 'post_title' => $this->data['name'], |
| 546 | 677 | 'post_type' => $this->post_type, |
| 547 | 678 | 'post_status' => 'publish', |
| 548 | - 'post_author' => 1, | |
| 549 | 679 | ]; |
| 550 | 680 | |
| 551 | 681 | if ( ! empty( $this->id ) ) { |
| 552 | 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(); | |
| 553 | 686 | } |
| 554 | 687 | |
| 555 | 688 | // Insert or Update appointment. |
| 556 | 689 | $appoint_id = wp_insert_post( $args ); |
| @@ -642,9 +775,10 @@ | ||
| 642 | 775 | ]; |
| 643 | 776 | |
| 644 | 777 | $args = wp_parse_args( $args, $defaults ); |
| 645 | 778 | |
| 646 | - 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'] ) { | |
| 647 | 781 | $args['meta_query'][] = [ |
| 648 | 782 | 'key' => '_tt_apointment_staff', |
| 649 | 783 | 'value' => $args['staff'], |
| 650 | 784 | 'compare' => 'LIKE', |
| @@ -712,11 +846,14 @@ | ||
| 712 | 846 | 'fleunt_crm_webhook' => $this->get_fleunt_crm_webhook(), |
| 713 | 847 | 'pabbly_hook_overwrite' => $this->get_pabbly_hook_overwrite(), |
| 714 | 848 | 'pabbly_webook' => $this->get_pabbly_webook(), |
| 715 | 849 | 'zapier_webook' => $this->get_zapier_webook(), |
| 850 | + 'flowmattic_webhook' => $this->get_flowmattic_webhook(), | |
| 851 | + 'flowmattic_hook_overwrite' => $this->get_flowmattic_hook_overwrite(), | |
| 716 | 852 | 'min_notice_time' => $this->get_min_notice_time(), |
| 717 | 853 | 'zapier_hook_overwrite' => $this->get_zapier_hook_overwrite(), |
| 718 | - 'permalink' => $this->get_appointment_permalink(), | |
| 854 | + 'guest_enabled' => $this->get_guest_enabled(), | |
| 855 | + 'guest_limit' => $this->get_guest_limit(), | |
| 719 | 856 | ] ); |
| 720 | 857 | |
| 721 | 858 | $this->set_id( 0 ); |
| 722 | 859 | $this->save(); |
| @@ -748,31 +885,42 @@ | ||
| 748 | 885 | * Get meeting schedule by date |
| 749 | 886 | * |
| 750 | 887 | * @param string $date [$date description] |
| 751 | 888 | * @param integer $staff_id [$staff_id description] |
| 889 | + * @param string $timze_zone [$timze_zone description] | |
| 752 | 890 | * |
| 753 | 891 | * @return array |
| 754 | 892 | */ |
| 755 | - private function get_schedule_by_date( $date, $staff_id, $timze_zone = '' ) { | |
| 756 | - $day_name = ucfirst( date( 'D', strtotime( $date ) ) ); | |
| 757 | - $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(); | |
| 758 | 896 | $interval = $this->get_interval(); |
| 759 | - $tartget_schedule = []; | |
| 897 | + $target_schedule = []; | |
| 760 | 898 | |
| 761 | 899 | $slots = []; |
| 762 | 900 | |
| 763 | 901 | if ( ! empty( $schedule[$staff_id] ) ) { |
| 764 | - $tartget_schedule = $schedule[$staff_id][$day_name]; | |
| 902 | + $target_schedule = $schedule[$staff_id][$day_name]; | |
| 765 | 903 | } |
| 766 | 904 | |
| 767 | - foreach ( $tartget_schedule as $day ) { | |
| 905 | + foreach ( $target_schedule as $day ) { | |
| 768 | 906 | $start = strtotime( $day['start'] ); |
| 769 | 907 | $end = strtotime( $day['end'] ); |
| 770 | 908 | |
| 771 | - 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 ) { | |
| 772 | 920 | $booked_entry = $this->get_booking_entries( $date, $time, $staff_id ); |
| 773 | 921 | $status = 'available'; |
| 774 | - $capacity = $this->get_capacity(); | |
| 922 | + $capacity = $this->get_effective_capacity(); | |
| 775 | 923 | $booked = 0; |
| 776 | 924 | |
| 777 | 925 | if ( $booked_entry ) { |
| 778 | 926 | if ( $booked_entry->get_booked() >= $capacity ) { |
| @@ -781,9 +929,11 @@ | ||
| 781 | 929 | |
| 782 | 930 | $booked = $booked_entry->get_booked(); |
| 783 | 931 | } |
| 784 | 932 | |
| 785 | - $datetime = $this->convert_timezone( $time, $timze_zone ); | |
| 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 ); | |
| 786 | 936 | |
| 787 | 937 | $slot = [ |
| 788 | 938 | 'status' => $status, |
| 789 | 939 | 'start_time' => $datetime->format( 'g:ia' ), |
| @@ -812,23 +962,29 @@ | ||
| 812 | 962 | * |
| 813 | 963 | * @return bool |
| 814 | 964 | */ |
| 815 | 965 | private function get_booking_entries( $date, $time, $staff_id ) { |
| 816 | - $time = gmdate( 'H:i', $time ); | |
| 966 | + $time = gmdate( 'h:i a', $time ); | |
| 817 | 967 | $booking_entries = new Booking_Entry(); |
| 818 | 968 | $entries = $booking_entries->find( [ |
| 819 | - 'meeting_id' => $this->id, | |
| 820 | 969 | 'staff_id' => $staff_id, |
| 821 | - 'date' => $date, | |
| 970 | + 'date' => $this->convert_timezone( $date, $this->get_timezone())->format('Y-m-d'), | |
| 822 | 971 | ] ); |
| 823 | 972 | |
| 824 | 973 | foreach ( $entries as $entry ) { |
| 825 | - $booking = new Booking( $entry->get_booking_id() ); | |
| 826 | - $booking_time = timetics_convert_timezone( $booking->get_start_date() . ' ' . $entry->get_start(), $booking->get_timezone(), $this->get_timezone() )->format( 'H:i' ); | |
| 974 | + $start_time = $entry->get_start(); | |
| 975 | + $end_time = $entry->get_end(); | |
| 827 | 976 | |
| 828 | - if ( $booking_time == $time ) { | |
| 829 | - return $entry; | |
| 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; | |
| 830 | 980 | } |
| 981 | + | |
| 982 | + if ( $this->is_orphan_entry( $entry ) ) { | |
| 983 | + continue; | |
| 984 | + } | |
| 985 | + | |
| 986 | + return $entry; | |
| 831 | 987 | } |
| 832 | 988 | |
| 833 | 989 | return false; |
| 834 | 990 | } |
| @@ -833,8 +989,47 @@ | ||
| 833 | 989 | return false; |
| 834 | 990 | } |
| 835 | 991 | |
| 836 | 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 | + /** | |
| 837 | 1032 | * Convert timestamp in target timezone |
| 838 | 1033 | * |
| 839 | 1034 | * @param integer $timestamp |
| 840 | 1035 | * @param string $timze_zone |
| @@ -851,6 +1046,61 @@ | ||
| 851 | 1046 | $datetime->setTimezone( new \DateTimeZone( $timze_zone ) ); |
| 852 | 1047 | |
| 853 | 1048 | // Get the converted timestamp |
| 854 | 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; | |
| 855 | 1105 | } |
| 856 | 1106 | } |