| @@ -1,5 +1,7 @@ | ||
| 1 | 1 | <?php |
| 2 | +defined( 'ABSPATH' ) || exit; | |
| 3 | + | |
| 2 | 4 | use function Timetics\timetics; |
| 3 | 5 | use Timetics\Core\Bookings\Booking; |
| 4 | 6 | use Timetics\Core\Integrations\Google\Client; |
| 5 | 7 | use Timetics\Core\Integrations\Google\Service\Calendar; |
| @@ -47,26 +49,47 @@ | ||
| 47 | 49 | */ |
| 48 | 50 | function timetics_get_google_access_token( $user_id = 0 ) { |
| 49 | 51 | $data = timetics_get_google_auth( $user_id ); |
| 50 | 52 | |
| 51 | - if ( ! $data ) { | |
| 53 | + if ( empty( $data ) || empty( $data['access_token'] ) ) { | |
| 52 | 54 | return false; |
| 53 | 55 | } |
| 54 | 56 | |
| 55 | - if ( ( $data['expires_in'] - 30 ) < time() ) { | |
| 56 | - $refresh_toekn = timetics_get_google_refresh_token( $user_id ); | |
| 57 | - $client = timetics_get_google_client(); | |
| 57 | + $expires_in = isset( $data['expires_in'] ) ? (int) $data['expires_in'] : 0; | |
| 58 | 58 | |
| 59 | - $data = $client->fetch_access_token_with_refresh_token( $refresh_toekn ); | |
| 59 | + // Token still valid (30s safety margin) — use it as-is. | |
| 60 | + if ( ( $expires_in - 30 ) > time() ) { | |
| 61 | + return $data['access_token']; | |
| 62 | + } | |
| 60 | 63 | |
| 61 | - if ( ! $data ) { | |
| 64 | + // Needs a refresh. | |
| 65 | + $refresh_token = timetics_get_google_refresh_token( $user_id ); | |
| 66 | + | |
| 67 | + if ( empty( $refresh_token ) ) { | |
| 68 | + return false; | |
| 69 | + } | |
| 70 | + | |
| 71 | + $client = timetics_get_google_client(); | |
| 72 | + $response = $client->fetch_access_token_with_refresh_token( $refresh_token ); | |
| 73 | + | |
| 74 | + if ( is_wp_error( $response ) ) { | |
| 75 | + $error_data = $response->get_error_data(); | |
| 76 | + $transient = ! is_array( $error_data ) || ! empty( $error_data['transient'] ); | |
| 77 | + | |
| 78 | + if ( $transient ) { | |
| 62 | 79 | return false; |
| 63 | 80 | } |
| 64 | 81 | |
| 65 | - timetics_update_google_auth( $user_id, $data ); | |
| 82 | + // Permanent failure (invalid_grant): the refresh token is dead and the account genuinely needs to be re-connected. Flag it so the UI can reflect the real state. | |
| 83 | + update_user_meta( $user_id, 'timetics_google_auth_error', $response->get_error_code() ); | |
| 84 | + | |
| 85 | + return false; | |
| 66 | 86 | } |
| 67 | 87 | |
| 68 | - return $data['access_token']; | |
| 88 | + // Persisting a fresh credential also clears any stale auth-error flag | |
| 89 | + timetics_update_google_auth( $user_id, $response ); | |
| 90 | + | |
| 91 | + return $response['access_token']; | |
| 69 | 92 | } |
| 70 | 93 | } |
| 71 | 94 | |
| 72 | 95 | if ( ! function_exists( 'timetics_get_google_refresh_token' ) ) { |
| @@ -105,8 +128,12 @@ | ||
| 105 | 128 | * |
| 106 | 129 | * @return void |
| 107 | 130 | */ |
| 108 | 131 | function timetics_update_google_auth( $user_id = 0, $data = [] ) { |
| 132 | + if ( empty( $data ) || ! is_array( $data ) ) { | |
| 133 | + return; | |
| 134 | + } | |
| 135 | + | |
| 109 | 136 | if ( ! empty( $data['code'] ) ) { |
| 110 | 137 | update_user_meta( $user_id, 'timetics_google_auth_code', $data['code'] ); |
| 111 | 138 | } |
| 112 | 139 | |
| @@ -113,11 +140,17 @@ | ||
| 113 | 140 | if ( ! empty( $data['refresh_token'] ) ) { |
| 114 | 141 | update_user_meta( $user_id, 'timetics_google_refresh_token', $data['refresh_token'] ); |
| 115 | 142 | } |
| 116 | 143 | |
| 117 | - $data['expires_in'] = $data['expires_in'] + time(); | |
| 144 | + // Google returns expires_in as a lifetime in seconds (~3600). | |
| 145 | + // Guard against a missing/zero value: storing time() alone would make the | |
| 146 | + // token look permanently expired, forcing a refresh on every request and | |
| 147 | + // hammering Google's token endpoint until it rate-limits the app. | |
| 148 | + $lifetime = ! empty( $data['expires_in'] ) ? (int) $data['expires_in'] : 3600; | |
| 149 | + $data['expires_in'] = time() + $lifetime; | |
| 118 | 150 | |
| 119 | 151 | update_user_meta( $user_id, 'timetics_google_auth', $data ); |
| 152 | + delete_user_meta( $user_id, 'timetics_google_auth_error' ); | |
| 120 | 153 | } |
| 121 | 154 | } |
| 122 | 155 | |
| 123 | 156 | if ( ! function_exists( 'timetics_get_google_auth' ) ) { |
| @@ -166,11 +199,11 @@ | ||
| 166 | 199 | * |
| 167 | 200 | * @return string |
| 168 | 201 | */ |
| 169 | 202 | function timetics_get_google_auth_url() { |
| 170 | - $client = timetics_get_google_client(); | |
| 203 | + $local_url = site_url( 'timetics-integration/google-auth' ); | |
| 171 | 204 | |
| 172 | - return $client->get_auth_url(); | |
| 205 | + return wp_nonce_url( $local_url, 'timetics_auth_action', 'timetics_auth_nonce' ); | |
| 173 | 206 | } |
| 174 | 207 | } |
| 175 | 208 | |
| 176 | 209 | if ( ! function_exists( 'timetics_get_posts' ) ) { |
| @@ -312,8 +345,70 @@ | ||
| 312 | 345 | return $total; |
| 313 | 346 | } |
| 314 | 347 | } |
| 315 | 348 | |
| 349 | +if ( ! function_exists( 'timetics_can_view_all_data' ) ) { | |
| 350 | + /** | |
| 351 | + * Check the current user is allowed to see site wide data | |
| 352 | + * | |
| 353 | + * Staff hold `manage_timetics`, so that capability cannot be used to tell an | |
| 354 | + * administrator apart from a team member. | |
| 355 | + * | |
| 356 | + * @return bool | |
| 357 | + */ | |
| 358 | + function timetics_can_view_all_data() { | |
| 359 | + return current_user_can( 'manage_options' ); | |
| 360 | + } | |
| 361 | +} | |
| 362 | + | |
| 363 | +if ( ! function_exists( 'timetics_get_visible_booking_ids' ) ) { | |
| 364 | + /** | |
| 365 | + * Get booking ids a user is allowed to see | |
| 366 | + * | |
| 367 | + * @param integer $user_id | |
| 368 | + * | |
| 369 | + * @return array Never empty, so it is always safe to pass to `post__in`. | |
| 370 | + */ | |
| 371 | + function timetics_get_visible_booking_ids( $user_id = 0 ) { | |
| 372 | + if ( ! $user_id ) { | |
| 373 | + $user_id = get_current_user_id(); | |
| 374 | + } | |
| 375 | + | |
| 376 | + $booking_ids = Booking::get_visible_ids_for_user( $user_id ); | |
| 377 | + | |
| 378 | + // WP_Query ignores an empty post__in and returns everything, so fall back to a | |
| 379 | + // non-existent id to mean "nothing visible". | |
| 380 | + return ! empty( $booking_ids ) ? $booking_ids : [0]; | |
| 381 | + } | |
| 382 | +} | |
| 383 | + | |
| 384 | +if ( ! function_exists( 'timetics_get_visible_customer_ids' ) ) { | |
| 385 | + /** | |
| 386 | + * Get customer user ids a user is allowed to see, derived from their visible bookings | |
| 387 | + * | |
| 388 | + * @param integer $user_id | |
| 389 | + * | |
| 390 | + * @return array Never empty, so it is always safe to pass to `include`. | |
| 391 | + */ | |
| 392 | + function timetics_get_visible_customer_ids( $user_id = 0 ) { | |
| 393 | + $booking_ids = timetics_get_visible_booking_ids( $user_id ); | |
| 394 | + $customer_ids = []; | |
| 395 | + | |
| 396 | + foreach ( $booking_ids as $booking_id ) { | |
| 397 | + $customer_id = (int) get_post_meta( $booking_id, '_tt_booking_customer', true ); | |
| 398 | + | |
| 399 | + if ( $customer_id ) { | |
| 400 | + $customer_ids[] = $customer_id; | |
| 401 | + } | |
| 402 | + } | |
| 403 | + | |
| 404 | + $customer_ids = array_values( array_unique( $customer_ids ) ); | |
| 405 | + | |
| 406 | + // WP_User_Query ignores an empty include and returns everything. | |
| 407 | + return ! empty( $customer_ids ) ? $customer_ids : [0]; | |
| 408 | + } | |
| 409 | +} | |
| 410 | + | |
| 316 | 411 | if ( ! function_exists( 'timetics_get_staff_integrations' ) ) { |
| 317 | 412 | /** |
| 318 | 413 | * Get all staff integrations |
| 319 | 414 | * |
| @@ -319,9 +414,15 @@ | ||
| 319 | 414 | * |
| 320 | 415 | * @return array |
| 321 | 416 | */ |
| 322 | 417 | function timetics_get_staff_integrations( $user_id = 0 ) { |
| 323 | - $google_auth = timetics_get_google_access_token( $user_id ); | |
| 418 | + timetics_get_google_access_token( $user_id ); | |
| 419 | + | |
| 420 | + $refresh_token = timetics_get_google_refresh_token( $user_id ); | |
| 421 | + $auth_error = get_user_meta( $user_id, 'timetics_google_auth_error', true ); | |
| 422 | + $is_connected = ! empty( $refresh_token ) && empty( $auth_error ); | |
| 423 | + $needs_reauth = ! empty( $refresh_token ) && ! empty( $auth_error ); | |
| 424 | + | |
| 324 | 425 | $google_credentials = timetics_get_google_credentials(); |
| 325 | 426 | $is_setup = ! empty( $google_credentials['client_id'] ) && ! empty( $google_credentials['client_secret'] ); |
| 326 | 427 | $current_user_id = ( $user_id == get_current_user_id() ) ? true : false; |
| 327 | 428 | |
| @@ -330,9 +431,10 @@ | ||
| 330 | 431 | 'id' => 'google-calendar-meet', |
| 331 | 432 | 'name' => esc_html__( 'Google Meet', 'timetics' ), |
| 332 | 433 | 'description' => esc_html__( 'Connect your Meet to sync your booked events.', 'timetics' ), |
| 333 | 434 | 'auth_url' => timetics_get_google_auth_url(), |
| 334 | - 'connected' => ! empty( $google_auth ), | |
| 435 | + 'connected' => $is_connected, | |
| 436 | + 'needs_reauth' => $needs_reauth, | |
| 335 | 437 | 'setup' => $is_setup, |
| 336 | 438 | 'current_user' => $current_user_id, |
| 337 | 439 | ], |
| 338 | 440 | [ |
| @@ -339,9 +441,10 @@ | ||
| 339 | 441 | 'id' => 'google-calendar', |
| 340 | 442 | 'name' => esc_html__( 'Google Calendar', 'timetics' ), |
| 341 | 443 | 'description' => esc_html__( 'Connect your Meet to sync your booked events.', 'timetics' ), |
| 342 | 444 | 'auth_url' => timetics_get_google_auth_url(), |
| 343 | - 'connected' => ! empty( $google_auth ), | |
| 445 | + 'connected' => $is_connected, | |
| 446 | + 'needs_reauth' => $needs_reauth, | |
| 344 | 447 | 'setup' => $is_setup, |
| 345 | 448 | 'current_user' => $current_user_id, |
| 346 | 449 | ], |
| 347 | 450 | ]; |
| @@ -403,12 +506,14 @@ | ||
| 403 | 506 | */ |
| 404 | 507 | function timetics_update_default_settings() { |
| 405 | 508 | $settings = [ |
| 406 | 509 | 'google_auth_redirect_uri' => timetics_get_auth_redirect_uri( 'google-auth' ), |
| 510 | + 'outlook_auth_redirect_uri'=> timetics_get_auth_redirect_uri( 'outlook-auth' ), | |
| 407 | 511 | 'default_booking_status' => 'pending', |
| 408 | 512 | 'currency' => 'USD', |
| 409 | 513 | 'primary_color' => '#3161F1', |
| 410 | 514 | 'secondary_color' => '#6188ff', |
| 515 | + 'unpaid_booking_expiry_minutes' => 5, | |
| 411 | 516 | ]; |
| 412 | 517 | |
| 413 | 518 | $settings = apply_filters( 'timetics_default_settings', $settings ); |
| 414 | 519 | |
| @@ -510,8 +615,14 @@ | ||
| 510 | 615 | * @return void |
| 511 | 616 | */ |
| 512 | 617 | function timetics_register_cron() { |
| 513 | 618 | wp_schedule_event( time(), 'daily', 'timetics_booking_clear_schedule' ); |
| 619 | + | |
| 620 | + // Guarded: Hooks::maybe_schedule_cleanup_cron() also calls this on | |
| 621 | + // every 'init', so without the guard it'd stack duplicate events. | |
| 622 | + if ( ! wp_next_scheduled( 'timetics_cleanup_unpaid_bookings' ) ) { | |
| 623 | + wp_schedule_event( time(), 'timetics_five_minutes', 'timetics_cleanup_unpaid_bookings' ); | |
| 624 | + } | |
| 514 | 625 | } |
| 515 | 626 | } |
| 516 | 627 | |
| 517 | 628 | if ( ! function_exists( 'timetics_is_woocommerce_active' ) ) { |
| @@ -719,11 +830,307 @@ | ||
| 719 | 830 | $user_data = [ |
| 720 | 831 | 'id' => $user->ID, |
| 721 | 832 | 'username' => $user->user_login, |
| 722 | 833 | 'email' => $user->user_email, |
| 723 | - 'roles' => ['administrator'], | |
| 834 | + 'roles' => $user-> roles, | |
| 724 | 835 | ]; |
| 836 | + return $user_data; | |
| 837 | + } | |
| 838 | +} | |
| 725 | 839 | |
| 726 | - return $user_data; | |
| 840 | +if ( ! function_exists( 'timetics_connected_platforms' ) ) { | |
| 841 | + | |
| 842 | + /** | |
| 843 | + * Get connected platforms | |
| 844 | + * | |
| 845 | + * @param integer $id | |
| 846 | + * | |
| 847 | + * @return array Connected platform lists | |
| 848 | + */ | |
| 849 | + function timetics_connected_platforms( $id ) { | |
| 850 | + $integrations = timetics_get_staff_integrations( $id ); | |
| 851 | + $integration_names = []; | |
| 852 | + | |
| 853 | + if ( $integrations ) { | |
| 854 | + foreach ( $integrations as $integration ) { | |
| 855 | + if ( $integration['connected'] ) { | |
| 856 | + $integration_names[] = $integration['id']; | |
| 857 | + } | |
| 858 | + } | |
| 859 | + } | |
| 860 | + | |
| 861 | + return $integration_names; | |
| 727 | 862 | } |
| 728 | 863 | } |
| 729 | 864 | |
| 865 | +if ( ! function_exists( 'timetics_modules_list' ) ) { | |
| 866 | + /** | |
| 867 | + * Get the master list of Arraytics plugins for the About Us page. | |
| 868 | + * | |
| 869 | + * Each entry describes an external Arraytics plugin that can be | |
| 870 | + * installed/activated/deactivated from within the Timetics admin. | |
| 871 | + * | |
| 872 | + * @return array | |
| 873 | + */ | |
| 874 | + function timetics_modules_list(): array { | |
| 875 | + return [ | |
| 876 | + 'eventin' => [ | |
| 877 | + 'name' => 'eventin', | |
| 878 | + 'slug' => 'wp-event-solution', | |
| 879 | + 'type' => 'plugin', | |
| 880 | + 'upgrade' => false, | |
| 881 | + 'upgrade_link' => 'https://themewinter.com/eventin', | |
| 882 | + 'status' => 'on', | |
| 883 | + 'is_pro' => false, | |
| 884 | + 'title' => __( 'Eventin', 'timetics' ), | |
| 885 | + 'description' => __( 'Best Free WordPress Event Calendar Plugin to manage booking, registration, RSVPs, schedule recurring events and sell event tickets with WooCommerce.', 'timetics' ), | |
| 886 | + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'eventin' ), | |
| 887 | + 'notice' => '', | |
| 888 | + 'demo_link' => 'https://product.themewinter.com/eventin', | |
| 889 | + 'settings_link' => '', | |
| 890 | + 'doc_link' => 'https://support.themewinter.com/docs/plugins/docs/eventin/', | |
| 891 | + ], | |
| 892 | + 'booktics' => [ | |
| 893 | + 'name' => 'booktics', | |
| 894 | + 'slug' => 'booktics', | |
| 895 | + 'type' => 'plugin', | |
| 896 | + 'upgrade' => false, | |
| 897 | + 'upgrade_link' => 'https://arraytics.com/booktics', | |
| 898 | + 'status' => 'on', | |
| 899 | + 'is_pro' => false, | |
| 900 | + 'title' => __( 'Booktics', 'timetics' ), | |
| 901 | + 'description' => __( 'The ultimate online booking plugin for WordPress — services, staff, schedules, and payments in one place.', 'timetics' ), | |
| 902 | + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'booktics' ), | |
| 903 | + 'notice' => '', | |
| 904 | + 'demo_link' => 'https://arraytics.com/booktics', | |
| 905 | + 'settings_link' => '', | |
| 906 | + 'doc_link' => 'https://docs.arraytics.com/docs/booktics/', | |
| 907 | + ], | |
| 908 | + 'poptics' => [ | |
| 909 | + 'name' => 'poptics', | |
| 910 | + 'slug' => 'poptics', | |
| 911 | + 'type' => 'plugin', | |
| 912 | + 'upgrade' => false, | |
| 913 | + 'upgrade_link' => 'https://arraytics.com/poptics', | |
| 914 | + 'status' => 'on', | |
| 915 | + 'is_pro' => false, | |
| 916 | + 'title' => __( 'Poptics', 'timetics' ), | |
| 917 | + 'description' => __( 'Discover the ultimate popup mix with Poptics Popup Builder to boost your lead generation and sales conversions.', 'timetics' ), | |
| 918 | + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'poptics' ), | |
| 919 | + 'notice' => '', | |
| 920 | + 'demo_link' => 'https://arraytics.com/poptics', | |
| 921 | + 'settings_link' => '', | |
| 922 | + 'doc_link' => 'https://docs.aethonic.com/docs/getting-started/intro/', | |
| 923 | + ], | |
| 924 | + 'wpcafe' => [ | |
| 925 | + 'name' => 'wpcafe', | |
| 926 | + 'slug' => 'wp-cafe', | |
| 927 | + 'type' => 'plugin', | |
| 928 | + 'upgrade' => false, | |
| 929 | + 'upgrade_link' => 'https://themewinter.com/wpcafe', | |
| 930 | + 'status' => 'on', | |
| 931 | + 'is_pro' => false, | |
| 932 | + 'title' => __( 'WPCafe', 'timetics' ), | |
| 933 | + 'description' => __( 'WPCafe — Restaurant plugin with Online Ordering, Delivery, Pickup, Food Menu, Reservations, and Table Management.', 'timetics' ), | |
| 934 | + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'wpcafe' ), | |
| 935 | + 'notice' => '', | |
| 936 | + 'demo_link' => 'https://product.themewinter.com/wpcafe', | |
| 937 | + 'settings_link' => '', | |
| 938 | + 'doc_link' => 'https://support.themewinter.com/docs/plugins/docs/wp-cafe/', | |
| 939 | + ], | |
| 940 | + 'aisentic' => [ | |
| 941 | + 'name' => 'aisentic', | |
| 942 | + 'slug' => 'aisentic', | |
| 943 | + 'type' => 'plugin', | |
| 944 | + 'upgrade' => false, | |
| 945 | + 'upgrade_link' => '', | |
| 946 | + 'status' => 'on', | |
| 947 | + 'is_pro' => false, | |
| 948 | + 'title' => __( 'Aisentic', 'timetics' ), | |
| 949 | + 'description' => __( 'Your AI assistant for Timetics. Manage bookings, analyze schedules and automate routine tasks using plain English — get work done in seconds instead of hours.', 'timetics' ), | |
| 950 | + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'aisentic' ), | |
| 951 | + 'notice' => '', | |
| 952 | + 'demo_link' => '', | |
| 953 | + 'settings_link' => '', | |
| 954 | + 'doc_link' => 'https://support.themewinter.com/docs/plugins/docs/aisentic/', | |
| 955 | + ], | |
| 956 | + 'optiontics' => [ | |
| 957 | + 'name' => 'optiontics', | |
| 958 | + 'slug' => 'optiontics', | |
| 959 | + 'type' => 'plugin', | |
| 960 | + 'upgrade' => false, | |
| 961 | + 'upgrade_link' => '', | |
| 962 | + 'status' => 'on', | |
| 963 | + 'is_pro' => false, | |
| 964 | + 'title' => __( 'Optiontics', 'timetics' ), | |
| 965 | + 'description' => __( 'Offer paid add-ons and extra options with every booking — services, extras, packages or custom upgrades that customers can pick during checkout.', 'timetics' ), | |
| 966 | + 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'optiontics' ), | |
| 967 | + 'notice' => '', | |
| 968 | + 'demo_link' => '', | |
| 969 | + 'settings_link' => '', | |
| 970 | + 'doc_link' => 'https://support.themewinter.com/docs/plugins/plugin-docs/optiontics/how-to-create-product-options/', | |
| 971 | + 'download_url' => 'https://github.com/themewinter/optiontics-public/releases/download/release/optiontics.zip', | |
| 972 | + ], | |
| 973 | + ]; | |
| 974 | + } | |
| 975 | +} | |
| 976 | + | |
| 977 | +if ( ! function_exists( 'timetics_extension' ) ) { | |
| 978 | + /** | |
| 979 | + * Get the Timetics Extension manager instance (tools-sdk). | |
| 980 | + * | |
| 981 | + * @return \Arraytics\ToolsSdk\Extension | |
| 982 | + */ | |
| 983 | + function timetics_extension(): \Arraytics\ToolsSdk\Extension { | |
| 984 | + return new \Arraytics\ToolsSdk\Extension( 'timetics_available_modules', timetics_modules_list() ); | |
| 985 | + } | |
| 986 | +} | |
| 987 | + | |
| 988 | +if ( ! function_exists( 'timetics_get_enabled_modules' ) ) { | |
| 989 | + /** | |
| 990 | + * Get list of enabled module slugs. | |
| 991 | + * | |
| 992 | + * @return array | |
| 993 | + */ | |
| 994 | + function timetics_get_enabled_modules(): array { | |
| 995 | + $modules = get_option( 'timetics_available_modules', [] ); | |
| 996 | + | |
| 997 | + if ( empty( $modules ) || ! is_array( $modules ) ) { | |
| 998 | + return []; | |
| 999 | + } | |
| 1000 | + | |
| 1001 | + $enabled = []; | |
| 1002 | + | |
| 1003 | + foreach ( $modules as $slug => $data ) { | |
| 1004 | + if ( is_array( $data ) && isset( $data['status'] ) && 'on' === $data['status'] ) { | |
| 1005 | + $enabled[] = $slug; | |
| 1006 | + } elseif ( is_string( $data ) && 'on' === $data ) { | |
| 1007 | + $enabled[] = $slug; | |
| 1008 | + } | |
| 1009 | + } | |
| 1010 | + | |
| 1011 | + return $enabled; | |
| 1012 | + } | |
| 1013 | +} | |
| 1014 | + | |
| 1015 | +if ( ! function_exists( 'timetics_wp_timezone_string' ) ) { | |
| 1016 | + /** | |
| 1017 | + * wp_timezone_string() polyfill — core's version needs WP 5.3, plugin supports 5.2. | |
| 1018 | + * | |
| 1019 | + * @return string | |
| 1020 | + */ | |
| 1021 | + function timetics_wp_timezone_string() { | |
| 1022 | + if ( function_exists( 'wp_timezone_string' ) ) { | |
| 1023 | + return wp_timezone_string(); | |
| 1024 | + } | |
| 1025 | + | |
| 1026 | + $timezone_string = get_option( 'timezone_string' ); | |
| 1027 | + | |
| 1028 | + if ( $timezone_string ) { | |
| 1029 | + return $timezone_string; | |
| 1030 | + } | |
| 1031 | + | |
| 1032 | + $offset = (float) get_option( 'gmt_offset' ); | |
| 1033 | + $hours = (int) $offset; | |
| 1034 | + $minutes = abs( ( $offset - $hours ) * 60 ); | |
| 1035 | + | |
| 1036 | + return sprintf( '%s%02d:%02d', ( $offset < 0 ) ? '-' : '+', abs( $hours ), $minutes ); | |
| 1037 | + } | |
| 1038 | +} | |
| 1039 | + | |
| 1040 | +if ( ! function_exists( 'timetics_reminder_fallback_timezone' ) ) { | |
| 1041 | + /** | |
| 1042 | + * Timezone to interpret a booking's stored wall clock in when the booking | |
| 1043 | + * itself carries no usable timezone. | |
| 1044 | + * | |
| 1045 | + * A booking normally stores the customer's timezone next to the customer's | |
| 1046 | + * local start time, and that pair must be read together. Bookings created | |
| 1047 | + * before that meta existed, or from the admin side, have no timezone — those | |
| 1048 | + * times were entered against the site clock, so the site timezone is the | |
| 1049 | + * closer reading. Falling back to UTC put every reminder on such a booking | |
| 1050 | + * out by the site's GMT offset. | |
| 1051 | + * | |
| 1052 | + * @return string A timezone identifier or ±hh:mm offset accepted by DateTimeZone. | |
| 1053 | + */ | |
| 1054 | + function timetics_reminder_fallback_timezone() { | |
| 1055 | + $timezone = timetics_wp_timezone_string(); | |
| 1056 | + | |
| 1057 | + return $timezone ? $timezone : 'UTC'; | |
| 1058 | + } | |
| 1059 | +} | |
| 1060 | + | |
| 1061 | +if ( ! function_exists( 'timetics_format_email_datetime' ) ) { | |
| 1062 | + /** | |
| 1063 | + * Format date, time and day according to WordPress admin settings | |
| 1064 | + * Used in email templates to display consistent date/time formats | |
| 1065 | + * | |
| 1066 | + * @param string $start_date Start date (Y-m-d format) | |
| 1067 | + * @param string $start_time Start time (h:i a format) | |
| 1068 | + * | |
| 1069 | + * @return array Array with 'day', 'time', 'date' keys | |
| 1070 | + */ | |
| 1071 | + function timetics_format_email_datetime( $start_date, $start_time = '' ) { | |
| 1072 | + $wp_date_format = get_option( 'date_format' ); | |
| 1073 | + $wp_time_format = get_option( 'time_format' ); | |
| 1074 | + | |
| 1075 | + $datetime = $start_time | |
| 1076 | + ? $start_date . ' ' . $start_time | |
| 1077 | + : $start_date; | |
| 1078 | + | |
| 1079 | + $timestamp = strtotime( $datetime ); | |
| 1080 | + | |
| 1081 | + return [ | |
| 1082 | + 'day' => date_i18n( 'l', $timestamp ), | |
| 1083 | + 'time' => date_i18n( $wp_time_format, $timestamp ), | |
| 1084 | + 'date' => date_i18n( $wp_date_format, $timestamp ), | |
| 1085 | + ]; | |
| 1086 | + } | |
| 1087 | +} | |
| 1088 | + | |
| 1089 | +if ( ! function_exists( 'timetics_aisentic_identity' ) ) { | |
| 1090 | + /** | |
| 1091 | + * Resolve the identity Timetics hands to Aisentic when the user connects. | |
| 1092 | + * | |
| 1093 | + * The consent UI shows these exact values before anything leaves the site, | |
| 1094 | + * so the connect request must read them from here too. Showing one email | |
| 1095 | + * and sending another would break the consent. | |
| 1096 | + * | |
| 1097 | + * The account belongs to the person who opts in, so it is the logged-in | |
| 1098 | + * user's name and email. The Ask AI setup dialog lets them edit the email, | |
| 1099 | + * which arrives here as $email. The site admin email is the last fallback. | |
| 1100 | + * | |
| 1101 | + * @param string $email Optional email the user typed in the consent UI. | |
| 1102 | + * @return array{name:string,email:string,site_url:string} | |
| 1103 | + */ | |
| 1104 | + function timetics_aisentic_identity( $email = '' ) { | |
| 1105 | + $user = wp_get_current_user(); | |
| 1106 | + $name = sanitize_text_field( (string) $user->display_name ); | |
| 1107 | + $email = sanitize_email( (string) ( $email ?: $user->user_email ?: get_option( 'admin_email', '' ) ) ); | |
| 1108 | + | |
| 1109 | + return [ | |
| 1110 | + 'name' => $name, | |
| 1111 | + 'email' => $email, | |
| 1112 | + 'site_url' => site_url(), | |
| 1113 | + ]; | |
| 1114 | + } | |
| 1115 | +} | |
| 1116 | + | |
| 1117 | +if ( ! function_exists( 'timetics_aisentic_is_registered' ) ) { | |
| 1118 | + /** | |
| 1119 | + * Whether Aisentic already holds a provider api key. | |
| 1120 | + * | |
| 1121 | + * Reads Aisentic's own settings, so a site the user registered from | |
| 1122 | + * Aisentic's own screens counts as connected as well. Without that the | |
| 1123 | + * dashboard banner would keep nagging people who are already set up. | |
| 1124 | + * | |
| 1125 | + * @return bool | |
| 1126 | + */ | |
| 1127 | + function timetics_aisentic_is_registered() { | |
| 1128 | + if ( function_exists( 'aisentic_get_option' ) ) { | |
| 1129 | + return ! empty( aisentic_get_option( 'llmProviders.aisentic.apiKey', '' ) ); | |
| 1130 | + } | |
| 1131 | + | |
| 1132 | + $settings = get_option( 'aisentic_settings', [] ); | |
| 1133 | + | |
| 1134 | + return ! empty( $settings['llmProviders']['aisentic']['apiKey'] ); | |
| 1135 | + } | |
| 1136 | +} | |