| @@ -345,8 +345,70 @@ | ||
| 345 | 345 | return $total; |
| 346 | 346 | } |
| 347 | 347 | } |
| 348 | 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 | + | |
| 349 | 411 | if ( ! function_exists( 'timetics_get_staff_integrations' ) ) { |
| 350 | 412 | /** |
| 351 | 413 | * Get all staff integrations |
| 352 | 414 | * |
| @@ -449,8 +511,9 @@ | ||
| 449 | 511 | 'default_booking_status' => 'pending', |
| 450 | 512 | 'currency' => 'USD', |
| 451 | 513 | 'primary_color' => '#3161F1', |
| 452 | 514 | 'secondary_color' => '#6188ff', |
| 515 | + 'unpaid_booking_expiry_minutes' => 5, | |
| 453 | 516 | ]; |
| 454 | 517 | |
| 455 | 518 | $settings = apply_filters( 'timetics_default_settings', $settings ); |
| 456 | 519 | |
| @@ -552,8 +615,14 @@ | ||
| 552 | 615 | * @return void |
| 553 | 616 | */ |
| 554 | 617 | function timetics_register_cron() { |
| 555 | 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 | + } | |
| 556 | 625 | } |
| 557 | 626 | } |
| 558 | 627 | |
| 559 | 628 | if ( ! function_exists( 'timetics_is_woocommerce_active' ) ) { |
| @@ -876,15 +945,14 @@ | ||
| 876 | 945 | 'upgrade_link' => '', |
| 877 | 946 | 'status' => 'on', |
| 878 | 947 | 'is_pro' => false, |
| 879 | 948 | 'title' => __( 'Aisentic', 'timetics' ), |
| 880 | - 'description' => __( 'AI assistant for WordPress — automate content, replies and on-site tasks without leaving your dashboard.', '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' ), | |
| 881 | 950 | 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'aisentic' ), |
| 882 | 951 | 'notice' => '', |
| 883 | 952 | 'demo_link' => '', |
| 884 | 953 | 'settings_link' => '', |
| 885 | 954 | 'doc_link' => 'https://support.themewinter.com/docs/plugins/docs/aisentic/', |
| 886 | - 'download_url' => 'https://github.com/themewinter/aisentic-public/releases/download/v1.0.0/aisentic-1.0.0.zip', | |
| 887 | 955 | ], |
| 888 | 956 | 'optiontics' => [ |
| 889 | 957 | 'name' => 'optiontics', |
| 890 | 958 | 'slug' => 'optiontics', |
| @@ -893,9 +961,9 @@ | ||
| 893 | 961 | 'upgrade_link' => '', |
| 894 | 962 | 'status' => 'on', |
| 895 | 963 | 'is_pro' => false, |
| 896 | 964 | 'title' => __( 'Optiontics', 'timetics' ), |
| 897 | - 'description' => __( 'Product add-ons / extras for food items — let customers pick toppings, sizes and other paid options along with the products.', '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' ), | |
| 898 | 966 | 'icon' => \Timetics\Core\Addon\Extension_Icon::get( 'optiontics' ), |
| 899 | 967 | 'notice' => '', |
| 900 | 968 | 'demo_link' => '', |
| 901 | 969 | 'settings_link' => '', |
| @@ -943,8 +1011,33 @@ | ||
| 943 | 1011 | return $enabled; |
| 944 | 1012 | } |
| 945 | 1013 | } |
| 946 | 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 | + | |
| 947 | 1040 | if ( ! function_exists( 'timetics_reminder_fallback_timezone' ) ) { |
| 948 | 1041 | /** |
| 949 | 1042 | * Timezone to interpret a booking's stored wall clock in when the booking |
| 950 | 1043 | * itself carries no usable timezone. |
| @@ -958,9 +1051,9 @@ | ||
| 958 | 1051 | * |
| 959 | 1052 | * @return string A timezone identifier or ±hh:mm offset accepted by DateTimeZone. |
| 960 | 1053 | */ |
| 961 | 1054 | function timetics_reminder_fallback_timezone() { |
| 962 | - $timezone = wp_timezone_string(); | |
| 1055 | + $timezone = timetics_wp_timezone_string(); | |
| 963 | 1056 | |
| 964 | 1057 | return $timezone ? $timezone : 'UTC'; |
| 965 | 1058 | } |
| 966 | 1059 | } |
| @@ -989,6 +1082,55 @@ | ||
| 989 | 1082 | 'day' => date_i18n( 'l', $timestamp ), |
| 990 | 1083 | 'time' => date_i18n( $wp_time_format, $timestamp ), |
| 991 | 1084 | 'date' => date_i18n( $wp_date_format, $timestamp ), |
| 992 | 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'] ); | |
| 993 | 1135 | } |
| 994 | 1136 | } |