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 +235 -19 1.0.17 → 1.0.64 View file →
@@ -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' => '',
@@ -209,9 +216,11 @@
209 216 *
210 217 * @return array
211 218 */
212 219 public function get_staff_ids() {
213 - return $this->get_prop( 'staff' );
220 + $ids = $this->get_prop( 'staff' );
221 +
222 + return is_array( $ids ) ? array_values( $ids ) : [];
214 223 }
215 224
216 225 /**
217 226 * Get duplicate number
@@ -231,8 +240,25 @@
231 240 return $this->get_prop( 'capacity' );
232 241 }
233 242
234 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 + /**
235 261 * Get location
236 262 *
237 263 * @since 1.0.0
238 264 *
@@ -337,8 +363,44 @@
337 363 public function get_buffer_time() {
338 364 return $this->get_prop( 'buffer_time' );
339 365 }
340 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 +
341 403 public function get_timezone() {
342 404 return $this->get_prop( 'timezone' );
343 405 }
344 406
@@ -348,9 +410,8 @@
348 410
349 411 public function get_visibility() {
350 412 return $this->get_prop( 'visibility' );
351 413 }
352 -
353 414 public function get_fleunt_crm_webhook() {
354 415 return $this->get_prop( 'fleunt_crm_webhook' );
355 416 }
356 417 public function get_fluent_hook_overwrite() {
@@ -367,8 +428,14 @@
367 428 }
368 429 public function get_zapier_hook_overwrite() {
369 430 return $this->get_prop( 'zapier_hook_overwrite' );
370 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 + }
371 438 public function get_min_notice_time() {
372 439 return $this->get_prop( 'min_notice_time' );
373 440 }
374 441 public function get_guest_enabled() {
@@ -396,8 +463,38 @@
396 463 return $this->get_prop( 'notifications' );
397 464 }
398 465
399 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 + /**
400 497 * Get appointment categories
401 498 *
402 499 * @since 1.0.0
403 500 *
@@ -466,9 +563,9 @@
466 563 */
467 564 public function get_author() {
468 565 $post = get_post( $this->id );
469 566
470 - return $post->post_author;
567 + return $post ? (int) $post->post_author : 0;
471 568 }
472 569
473 570 /**
474 571 * Get seat plan settings
@@ -578,13 +675,15 @@
578 675 $args = [
579 676 'post_title' => $this->data['name'],
580 677 'post_type' => $this->post_type,
581 678 'post_status' => 'publish',
582 - 'post_author' => get_current_user_id(),
583 679 ];
584 680
585 681 if ( ! empty( $this->id ) ) {
586 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();
587 686 }
588 687
589 688 // Insert or Update appointment.
590 689 $appoint_id = wp_insert_post( $args );
@@ -676,9 +775,10 @@
676 775 ];
677 776
678 777 $args = wp_parse_args( $args, $defaults );
679 778
680 - 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'] ) {
681 781 $args['meta_query'][] = [
682 782 'key' => '_tt_apointment_staff',
683 783 'value' => $args['staff'],
684 784 'compare' => 'LIKE',
@@ -746,8 +846,10 @@
746 846 'fleunt_crm_webhook' => $this->get_fleunt_crm_webhook(),
747 847 'pabbly_hook_overwrite' => $this->get_pabbly_hook_overwrite(),
748 848 'pabbly_webook' => $this->get_pabbly_webook(),
749 849 'zapier_webook' => $this->get_zapier_webook(),
850 + 'flowmattic_webhook' => $this->get_flowmattic_webhook(),
851 + 'flowmattic_hook_overwrite' => $this->get_flowmattic_hook_overwrite(),
750 852 'min_notice_time' => $this->get_min_notice_time(),
751 853 'zapier_hook_overwrite' => $this->get_zapier_hook_overwrite(),
752 854 'guest_enabled' => $this->get_guest_enabled(),
753 855 'guest_limit' => $this->get_guest_limit(),
@@ -783,31 +885,42 @@
783 885 * Get meeting schedule by date
784 886 *
785 887 * @param string $date [$date description]
786 888 * @param integer $staff_id [$staff_id description]
889 + * @param string $timze_zone [$timze_zone description]
787 890 *
788 891 * @return array
789 892 */
790 - private function get_schedule_by_date( $date, $staff_id, $timze_zone = '' ) {
791 - $day_name = ucfirst( date( 'D', strtotime( $date ) ) );
792 - $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();
793 896 $interval = $this->get_interval();
794 - $tartget_schedule = [];
897 + $target_schedule = [];
795 898
796 899 $slots = [];
797 900
798 901 if ( ! empty( $schedule[$staff_id] ) ) {
799 - $tartget_schedule = $schedule[$staff_id][$day_name];
902 + $target_schedule = $schedule[$staff_id][$day_name];
800 903 }
801 904
802 - foreach ( $tartget_schedule as $day ) {
905 + foreach ( $target_schedule as $day ) {
803 906 $start = strtotime( $day['start'] );
804 907 $end = strtotime( $day['end'] );
805 908
806 - 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 ) {
807 920 $booked_entry = $this->get_booking_entries( $date, $time, $staff_id );
808 921 $status = 'available';
809 - $capacity = $this->get_capacity();
922 + $capacity = $this->get_effective_capacity();
810 923 $booked = 0;
811 924
812 925 if ( $booked_entry ) {
813 926 if ( $booked_entry->get_booked() >= $capacity ) {
@@ -816,9 +929,11 @@
816 929
817 930 $booked = $booked_entry->get_booked();
818 931 }
819 932
820 - $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 );
821 936
822 937 $slot = [
823 938 'status' => $status,
824 939 'start_time' => $datetime->format( 'g:ia' ),
@@ -850,19 +965,26 @@
850 965 private function get_booking_entries( $date, $time, $staff_id ) {
851 966 $time = gmdate( 'h:i a', $time );
852 967 $booking_entries = new Booking_Entry();
853 968 $entries = $booking_entries->find( [
854 - 'meeting_id' => $this->id,
855 969 'staff_id' => $staff_id,
856 970 'date' => $this->convert_timezone( $date, $this->get_timezone())->format('Y-m-d'),
857 971 ] );
858 972
859 973 foreach ( $entries as $entry ) {
860 - $booking = new Booking( $entry->get_booking_id() );
974 + $start_time = $entry->get_start();
975 + $end_time = $entry->get_end();
861 976
862 - if ( $entry->get_start() == $time ) {
863 - 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;
864 980 }
981 +
982 + if ( $this->is_orphan_entry( $entry ) ) {
983 + continue;
984 + }
985 +
986 + return $entry;
865 987 }
866 988
867 989 return false;
868 990 }
@@ -867,8 +989,47 @@
867 989 return false;
868 990 }
869 991
870 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 + /**
871 1032 * Convert timestamp in target timezone
872 1033 *
873 1034 * @param integer $timestamp
874 1035 * @param string $timze_zone
@@ -885,6 +1046,61 @@
885 1046 $datetime->setTimezone( new \DateTimeZone( $timze_zone ) );
886 1047
887 1048 // Get the converted timestamp
888 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;
889 1105 }
890 1106 }