| @@ -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 | } |