| @@ -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 | |
| @@ -84,8 +85,10 @@ | ||
| 84 | 85 | 'pabbly_hook_overwrite' => '', |
| 85 | 86 | 'zapier_hook_overwrite' => '', |
| 86 | 87 | 'pabbly_webook' => '', |
| 87 | 88 | 'zapier_webook' => '', |
| 89 | + 'flowmattic_hook_overwrite' => '', | |
| 90 | + 'flowmattic_webhook' => '', | |
| 88 | 91 | 'min_notice_time' => '', |
| 89 | 92 | 'notifications' => [ |
| 90 | 93 | 'booking_created_email_form' => '', |
| 91 | 94 | 'booking_created_email_title' => '', |
| @@ -237,8 +240,25 @@ | ||
| 237 | 240 | return $this->get_prop( 'capacity' ); |
| 238 | 241 | } |
| 239 | 242 | |
| 240 | 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 | + /** | |
| 241 | 261 | * Get location |
| 242 | 262 | * |
| 243 | 263 | * @since 1.0.0 |
| 244 | 264 | * |
| @@ -408,8 +428,14 @@ | ||
| 408 | 428 | } |
| 409 | 429 | public function get_zapier_hook_overwrite() { |
| 410 | 430 | return $this->get_prop( 'zapier_hook_overwrite' ); |
| 411 | 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 | + } | |
| 412 | 438 | public function get_min_notice_time() { |
| 413 | 439 | return $this->get_prop( 'min_notice_time' ); |
| 414 | 440 | } |
| 415 | 441 | public function get_guest_enabled() { |
| @@ -537,9 +563,9 @@ | ||
| 537 | 563 | */ |
| 538 | 564 | public function get_author() { |
| 539 | 565 | $post = get_post( $this->id ); |
| 540 | 566 | |
| 541 | - return $post->post_author; | |
| 567 | + return $post ? (int) $post->post_author : 0; | |
| 542 | 568 | } |
| 543 | 569 | |
| 544 | 570 | /** |
| 545 | 571 | * Get seat plan settings |
| @@ -649,13 +675,15 @@ | ||
| 649 | 675 | $args = [ |
| 650 | 676 | 'post_title' => $this->data['name'], |
| 651 | 677 | 'post_type' => $this->post_type, |
| 652 | 678 | 'post_status' => 'publish', |
| 653 | - 'post_author' => get_current_user_id(), | |
| 654 | 679 | ]; |
| 655 | 680 | |
| 656 | 681 | if ( ! empty( $this->id ) ) { |
| 657 | 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(); | |
| 658 | 686 | } |
| 659 | 687 | |
| 660 | 688 | // Insert or Update appointment. |
| 661 | 689 | $appoint_id = wp_insert_post( $args ); |
| @@ -747,9 +775,10 @@ | ||
| 747 | 775 | ]; |
| 748 | 776 | |
| 749 | 777 | $args = wp_parse_args( $args, $defaults ); |
| 750 | 778 | |
| 751 | - 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'] ) { | |
| 752 | 781 | $args['meta_query'][] = [ |
| 753 | 782 | 'key' => '_tt_apointment_staff', |
| 754 | 783 | 'value' => $args['staff'], |
| 755 | 784 | 'compare' => 'LIKE', |
| @@ -817,8 +846,10 @@ | ||
| 817 | 846 | 'fleunt_crm_webhook' => $this->get_fleunt_crm_webhook(), |
| 818 | 847 | 'pabbly_hook_overwrite' => $this->get_pabbly_hook_overwrite(), |
| 819 | 848 | 'pabbly_webook' => $this->get_pabbly_webook(), |
| 820 | 849 | 'zapier_webook' => $this->get_zapier_webook(), |
| 850 | + 'flowmattic_webhook' => $this->get_flowmattic_webhook(), | |
| 851 | + 'flowmattic_hook_overwrite' => $this->get_flowmattic_hook_overwrite(), | |
| 821 | 852 | 'min_notice_time' => $this->get_min_notice_time(), |
| 822 | 853 | 'zapier_hook_overwrite' => $this->get_zapier_hook_overwrite(), |
| 823 | 854 | 'guest_enabled' => $this->get_guest_enabled(), |
| 824 | 855 | 'guest_limit' => $this->get_guest_limit(), |
| @@ -877,12 +908,12 @@ | ||
| 877 | 908 | |
| 878 | 909 | // Get buffer times in seconds |
| 879 | 910 | $buffer_before = $this->get_buffer_time_before_in_seconds(); |
| 880 | 911 | $buffer_after = $this->get_buffer_time_after_in_seconds(); |
| 881 | - | |
| 912 | + | |
| 882 | 913 | // Adjust the start time to include buffer before |
| 883 | 914 | $adjusted_start = $start + $buffer_before; |
| 884 | - | |
| 915 | + | |
| 885 | 916 | // Adjust the end time to ensure we have enough time for the slot + buffer after |
| 886 | 917 | $min_slot_duration = $interval + $buffer_after; |
| 887 | 918 | |
| 888 | 919 | for ( $time = $adjusted_start; $time + $min_slot_duration <= $end; $time += $interval + $buffer_after + $buffer_before ) { |
| @@ -887,9 +918,9 @@ | ||
| 887 | 918 | |
| 888 | 919 | for ( $time = $adjusted_start; $time + $min_slot_duration <= $end; $time += $interval + $buffer_after + $buffer_before ) { |
| 889 | 920 | $booked_entry = $this->get_booking_entries( $date, $time, $staff_id ); |
| 890 | 921 | $status = 'available'; |
| 891 | - $capacity = $this->get_capacity(); | |
| 922 | + $capacity = $this->get_effective_capacity(); | |
| 892 | 923 | $booked = 0; |
| 893 | 924 | |
| 894 | 925 | if ( $booked_entry ) { |
| 895 | 926 | if ( $booked_entry->get_booked() >= $capacity ) { |
| @@ -898,9 +929,11 @@ | ||
| 898 | 929 | |
| 899 | 930 | $booked = $booked_entry->get_booked(); |
| 900 | 931 | } |
| 901 | 932 | |
| 902 | - $datetime = $this->convert_timezone( $time, $time_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 ); | |
| 903 | 936 | |
| 904 | 937 | $slot = [ |
| 905 | 938 | 'status' => $status, |
| 906 | 939 | 'start_time' => $datetime->format( 'g:ia' ), |
| @@ -937,16 +970,21 @@ | ||
| 937 | 970 | 'date' => $this->convert_timezone( $date, $this->get_timezone())->format('Y-m-d'), |
| 938 | 971 | ] ); |
| 939 | 972 | |
| 940 | 973 | foreach ( $entries as $entry ) { |
| 941 | - $booking = new Booking( $entry->get_booking_id() ); | |
| 942 | 974 | $start_time = $entry->get_start(); |
| 943 | 975 | $end_time = $entry->get_end(); |
| 944 | 976 | |
| 945 | 977 | // Check if current time slot falls within the booking duration |
| 946 | - if ( $this->is_time_in_range( $time, $start_time, $end_time ) ) { | |
| 947 | - return $entry; | |
| 978 | + if ( ! $this->is_time_in_range( $time, $start_time, $end_time ) ) { | |
| 979 | + continue; | |
| 948 | 980 | } |
| 981 | + | |
| 982 | + if ( $this->is_orphan_entry( $entry ) ) { | |
| 983 | + continue; | |
| 984 | + } | |
| 985 | + | |
| 986 | + return $entry; | |
| 949 | 987 | } |
| 950 | 988 | |
| 951 | 989 | return false; |
| 952 | 990 | } |
| @@ -951,12 +989,34 @@ | ||
| 951 | 989 | return false; |
| 952 | 990 | } |
| 953 | 991 | |
| 954 | 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 | + /** | |
| 955 | 1015 | * Check if a time falls within a given time range |
| 956 | 1016 | * |
| 957 | 1017 | * @param string $current_time Current time in h:i a format |
| 958 | - * @param string $start_time Start time in h:i a format | |
| 1018 | + * @param string $start_time Start time in h:i a format | |
| 959 | 1019 | * @param string $end_time End time in h:i a format |
| 960 | 1020 | * |
| 961 | 1021 | * @return bool |
| 962 | 1022 | */ |
| @@ -1033,9 +1093,10 @@ | ||
| 1033 | 1093 | $start = strtotime( $day['start'] ); |
| 1034 | 1094 | $end = strtotime( $day['end'] ); |
| 1035 | 1095 | |
| 1036 | 1096 | for ( $time = $start; $time <= $end; $time += $interval ) { |
| 1037 | - $datetime = $this->convert_timezone( $time, $time_zone ); | |
| 1097 | + // Slot's own date, see get_schedule_by_date(). | |
| 1098 | + $datetime = $this->convert_timezone( $date . ' ' . gmdate( 'g:ia', $time ), $time_zone ); | |
| 1038 | 1099 | $slot = $datetime->format( 'g:ia' ); |
| 1039 | 1100 | $slots[] = $slot; |
| 1040 | 1101 | } |
| 1041 | 1102 | } |