# timetics/1.0.64/utils/global-helper.php

Timetics – Appointment Booking Calendar &amp; Scheduling, version 1.0.64. 1,137 lines.

- Page: https://pluginprobe.com/plugins/timetics/1.0.64/code/utils/global-helper.php
- Raw: https://pluginprobe.com/plugins/timetics/1.0.64/raw/utils/global-helper.php
- Modified: 2026-09-28T14:30:54+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/timetics/1.0.64/code/utils/global-helper.php#L10-L20`.

```php
<?php
defined( 'ABSPATH' ) || exit;

use function Timetics\timetics;
use Timetics\Core\Bookings\Booking;
use Timetics\Core\Integrations\Google\Client;
use Timetics\Core\Integrations\Google\Service\Calendar;

/**
 * Get timezone list.
 *
 * @since 1.0.0
 *
 * @return  array
 */
if ( ! function_exists( 'timetics_get_timezone_list' ) ) {
    function timetics_get_timezone_list() {
        $tzlist = wp_timezone_choice( 'UTC' );
        $doc    = new DOMDocument();
        $doc->loadHTML( $tzlist );

        $elements  = $doc->getElementsByTagName( 'option' );
        $timezones = [];

        if ( ! empty( $elements ) ) {
            foreach ( $elements as $element ) {
                $data = [
                    'value' => $element->getAttribute( 'value' ),
                    'name'  => $element->textContent, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
                ];

                $timezones[] = $data;
            }
        }

        return $timezones;
    }
}

if ( ! function_exists( 'timetics_get_google_access_token' ) ) {
    /**
     * Get google access token
     *
     * @param   integer  $user_id  Authenticate user id
     *
     * @since 1.0.0
     *
     * @return  string
     */
    function timetics_get_google_access_token( $user_id = 0 ) {
        $data = timetics_get_google_auth( $user_id );

        if ( empty( $data ) || empty( $data['access_token'] ) ) {
            return false;
        }

        $expires_in = isset( $data['expires_in'] ) ? (int) $data['expires_in'] : 0;

        // Token still valid (30s safety margin) — use it as-is.
        if ( ( $expires_in - 30 ) > time() ) {
            return $data['access_token'];
        }

        // Needs a refresh.
        $refresh_token = timetics_get_google_refresh_token( $user_id );

        if ( empty( $refresh_token ) ) {
            return false;
        }

        $client   = timetics_get_google_client();
        $response = $client->fetch_access_token_with_refresh_token( $refresh_token );

        if ( is_wp_error( $response ) ) {
            $error_data = $response->get_error_data();
            $transient  = ! is_array( $error_data ) || ! empty( $error_data['transient'] );

            if ( $transient ) {
                return false;
            }

            // 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.
            update_user_meta( $user_id, 'timetics_google_auth_error', $response->get_error_code() );

            return false;
        }

        // Persisting a fresh credential also clears any stale auth-error flag
        timetics_update_google_auth( $user_id, $response );

        return $response['access_token'];
    }
}

if ( ! function_exists( 'timetics_get_google_refresh_token' ) ) {

    /**
     * Get google auth refresh token
     *
     * @param   integer  $user_id  Authenticate user id
     *
     * @return  string
     */
    function timetics_get_google_refresh_token( $user_id ) {
        return get_user_meta( $user_id, 'timetics_google_refresh_token', true );
    }
}

if ( ! function_exists( 'timetics_get_auth_redirect_uri' ) ) {
    /**
     * Get auth redirect uri
     *
     * @param   string  $auth
     *
     * @return  string
     */
    function timetics_get_auth_redirect_uri( $auth = '' ) {
        return site_url( 'timetics-integration/' . $auth );
    }
}

if ( ! function_exists( 'timetics_update_google_auth' ) ) {
    /**
     * Update google auth token
     *
     * @param   integer  $user_id
     * @param   array  $data
     *
     * @return void
     */
    function timetics_update_google_auth( $user_id = 0, $data = [] ) {
        if ( empty( $data ) || ! is_array( $data ) ) {
            return;
        }

        if ( ! empty( $data['code'] ) ) {
            update_user_meta( $user_id, 'timetics_google_auth_code', $data['code'] );
        }

        if ( ! empty( $data['refresh_token'] ) ) {
            update_user_meta( $user_id, 'timetics_google_refresh_token', $data['refresh_token'] );
        }

        // Google returns expires_in as a lifetime in seconds (~3600).
        // Guard against a missing/zero value: storing time() alone would make the
        // token look permanently expired, forcing a refresh on every request and
        // hammering Google's token endpoint until it rate-limits the app.
        $lifetime           = ! empty( $data['expires_in'] ) ? (int) $data['expires_in'] : 3600;
        $data['expires_in'] = time() + $lifetime;

        update_user_meta( $user_id, 'timetics_google_auth', $data );
        delete_user_meta( $user_id, 'timetics_google_auth_error' );
    }
}

if ( ! function_exists( 'timetics_get_google_auth' ) ) {
    /**
     * Get google auth
     *
     * @param   integer  $user_id
     *
     * @return  array
     */
    function timetics_get_google_auth( $user_id = 0 ) {
        return get_user_meta( $user_id, 'timetics_google_auth', true );
    }
}

if ( ! function_exists( 'timetics_get_google_client' ) ) {
    /**
     * Get google auth client
     *
     * @return Timetics\Core\Integrations\Google\Client
     */
    function timetics_get_google_client() {
        $client_id     = timetics_get_option( 'google_app_client_id' );
        $client_secret = timetics_get_option( 'google_app_client_secret' );

        $redirect_uri = timetics_get_auth_redirect_uri( 'google-auth' );

        $client = new Client();
        $client->add_scope( Calendar::scope() );
        $client->set_auth_config(
            [
                'client_id'      => $client_id,
                'client_secrete' => $client_secret,
            ]
        );

        $client->set_redirect_uri( $redirect_uri );

        return $client;
    }
}

if ( ! function_exists( 'timetics_get_google_auth_url' ) ) {
    /**
     * Get google auth url
     *
     * @return  string
     */
    function timetics_get_google_auth_url() {
        $local_url = site_url( 'timetics-integration/google-auth' );

        return wp_nonce_url( $local_url, 'timetics_auth_action', 'timetics_auth_nonce' );
    }
}

if ( ! function_exists( 'timetics_get_posts' ) ) {
    /**
     * Get posts with date range
     *
     * @param   array  $args
     *
     * @return  Object
     */
    function timetics_get_posts( $args = [] ) {
        $defaults = [
            'post_type'   => 'post',
            'post_status' => 'publish',
            'nopaging'    => true,
            'date_range'  => '',
        ];

        $args = wp_parse_args( $args, $defaults );

        // Date range found.
        if ( $args['date_range'] ) {
            $from_date = $args['date_range']['start'];
            $to_date   = $args['date_range']['end'];

            $args['date_query'] = [
                'relation' => 'AND',
                [
                    'before'    => [
                        'year'  => gmdate( 'Y', strtotime( $to_date ) ),
                        'month' => gmdate( 'm', strtotime( $to_date ) ),
                        'day'   => gmdate( 'd', strtotime( $to_date ) ),
                    ],
                    'after'     => [
                        'year'  => gmdate( 'Y', strtotime( $from_date ) ),
                        'month' => gmdate( 'm', strtotime( $from_date ) ),
                        'day'   => gmdate( 'd', strtotime( $from_date ) ),
                    ],
                    'inclusive' => true,
                ],
            ];
        }

        // The Query
        $posts = new WP_Query( $args );

        return $posts;
    }
}

if ( ! function_exists( 'timetics_count_posts()' ) ) {
    /**
     * Count post with date range or without date range
     *
     * @param   array  $args  Required query params
     *
     * @return  integer
     */
    function timetics_count_posts( $args = [] ) {
        $posts = timetics_get_posts( $args );

        return $posts->post_count;
    }
}

if ( ! function_exists( 'timetics_count_users' ) ) {
    /**
     * Count user by role and date range
     *
     * @param   array  $args  Required query params
     *
     * @return  integer
     */
    function timetics_count_users( $args = [] ) {
        $defaults = [
            'role'        => '',
            'date_range'  => '',
            'count_total' => true,
        ];

        $args = wp_parse_args( $args, $defaults );

        // Date range found.
        if ( $args['date_range'] ) {
            $from_date = $args['date_range']['start'];
            $to_date   = $args['date_range']['end'];

            $args['date_query'] = [
                'relation' => 'AND',
                [
                    'before'    => [
                        'year'  => gmdate( 'Y', strtotime( $to_date ) ),
                        'month' => gmdate( 'm', strtotime( $to_date ) ),
                        'day'   => gmdate( 'd', strtotime( $to_date ) ),
                    ],
                    'after'     => [
                        'year'  => gmdate( 'Y', strtotime( $from_date ) ),
                        'month' => gmdate( 'm', strtotime( $from_date ) ),
                        'day'   => gmdate( 'd', strtotime( $from_date ) ),
                    ],
                    'inclusive' => true,
                ],
            ];
        }

        // The Query
        $user_query = new WP_User_Query( $args );

        // Return total users.
        return $user_query->total_users;
    }
}

if ( ! function_exists( 'timetics_get_total_sale' ) ) {
    /**
     * Get total sale with date range or without date range
     *
     * @param   array  $args Sales Required Args
     *
     * @return  integer
     */
    function timetics_get_total_sale( $args = [] ) {
        $defaults = [
            'post_type'   => 'timetics-booking',
            'post_status' => 'approved',
        ];

        $args = wp_parse_args( $args, $defaults );

        $posts = timetics_get_posts( $args );

        $total = 0;

        foreach ( $posts->posts as $post ) {
            $booking = new Booking( $post->ID );
            $total += floatval( $booking->get_total() );
        }

        return $total;
    }
}

if ( ! function_exists( 'timetics_can_view_all_data' ) ) {
    /**
     * Check the current user is allowed to see site wide data
     *
     * Staff hold `manage_timetics`, so that capability cannot be used to tell an
     * administrator apart from a team member.
     *
     * @return  bool
     */
    function timetics_can_view_all_data() {
        return current_user_can( 'manage_options' );
    }
}

if ( ! function_exists( 'timetics_get_visible_booking_ids' ) ) {
    /**
     * Get booking ids a user is allowed to see
     *
     * @param   integer  $user_id
     *
     * @return  array Never empty, so it is always safe to pass to `post__in`.
     */
    function timetics_get_visible_booking_ids( $user_id = 0 ) {
        if ( ! $user_id ) {
            $user_id = get_current_user_id();
        }

        $booking_ids = Booking::get_visible_ids_for_user( $user_id );

        // WP_Query ignores an empty post__in and returns everything, so fall back to a
        // non-existent id to mean "nothing visible".
        return ! empty( $booking_ids ) ? $booking_ids : [0];
    }
}

if ( ! function_exists( 'timetics_get_visible_customer_ids' ) ) {
    /**
     * Get customer user ids a user is allowed to see, derived from their visible bookings
     *
     * @param   integer  $user_id
     *
     * @return  array Never empty, so it is always safe to pass to `include`.
     */
    function timetics_get_visible_customer_ids( $user_id = 0 ) {
        $booking_ids  = timetics_get_visible_booking_ids( $user_id );
        $customer_ids = [];

        foreach ( $booking_ids as $booking_id ) {
            $customer_id = (int) get_post_meta( $booking_id, '_tt_booking_customer', true );

            if ( $customer_id ) {
                $customer_ids[] = $customer_id;
            }
        }

        $customer_ids = array_values( array_unique( $customer_ids ) );

        // WP_User_Query ignores an empty include and returns everything.
        return ! empty( $customer_ids ) ? $customer_ids : [0];
    }
}

if ( ! function_exists( 'timetics_get_staff_integrations' ) ) {
    /**
     * Get all staff integrations
     *
     * @return array
     */
    function timetics_get_staff_integrations( $user_id = 0 ) {
        timetics_get_google_access_token( $user_id );

        $refresh_token      = timetics_get_google_refresh_token( $user_id );
        $auth_error         = get_user_meta( $user_id, 'timetics_google_auth_error', true );
        $is_connected       = ! empty( $refresh_token ) && empty( $auth_error );
        $needs_reauth       = ! empty( $refresh_token ) && ! empty( $auth_error );

        $google_credentials = timetics_get_google_credentials();
        $is_setup           = ! empty( $google_credentials['client_id'] ) && ! empty( $google_credentials['client_secret'] );
        $current_user_id    = ( $user_id == get_current_user_id() ) ? true : false;

        $integrations = [
            [
                'id'           => 'google-calendar-meet',
                'name'         => esc_html__( 'Google Meet', 'timetics' ),
                'description'  => esc_html__( 'Connect your Meet to sync your booked events.', 'timetics' ),
                'auth_url'     => timetics_get_google_auth_url(),
                'connected'    => $is_connected,
                'needs_reauth' => $needs_reauth,
                'setup'        => $is_setup,
                'current_user' => $current_user_id,
            ],
            [
                'id'           => 'google-calendar',
                'name'         => esc_html__( 'Google Calendar', 'timetics' ),
                'description'  => esc_html__( 'Connect your Meet to sync your booked events.', 'timetics' ),
                'auth_url'     => timetics_get_google_auth_url(),
                'connected'    => $is_connected,
                'needs_reauth' => $needs_reauth,
                'setup'        => $is_setup,
                'current_user' => $current_user_id,
            ],
        ];

        $integrations = apply_filters( 'timetics_staff_integrations', $integrations, $user_id );

        return $integrations;
    }
}

if ( ! function_exists( 'timetics_get_google_credentials' ) ) {
    /**
     * Get google auth credentials
     *
     * @return  array
     */
    function timetics_get_google_credentials() {
        $client_id     = timetics_get_option( 'google_app_client_id' );
        $client_secret = timetics_get_option( 'google_app_client_secret' );

        return [
            'client_id'     => $client_id,
            'client_secret' => $client_secret,
        ];
    }
}

if ( ! function_exists( 'timetics_get_default_timezone' ) ) {
    /**
     * Get default timezone
     *
     * @return  string
     */
    function timetics_get_default_timezone() {
        $timezone = get_option( 'timezone_string' );
        return $timezone;
    }
}

if ( ! function_exists( 'timetics_get_currencies' ) ) {
    /**
     * Get currency list
     *
     * @return  array
     */
    function timetics_get_currencies() {
        $currencies = require_once dirname( __FILE__ ) . '/currency.php';

        return $currencies;
    }
}

if ( ! function_exists( 'timetics_update_default_settings' ) ) {

    /**
     * Update default settings
     *
     * @return void
     */
    function timetics_update_default_settings() {
        $settings = [
            'google_auth_redirect_uri' => timetics_get_auth_redirect_uri( 'google-auth' ),
            'outlook_auth_redirect_uri'=> timetics_get_auth_redirect_uri( 'outlook-auth' ),
            'default_booking_status'   => 'pending',
            'currency'                 => 'USD',
            'primary_color'            => '#3161F1',
            'secondary_color'          => '#6188ff',
            'unpaid_booking_expiry_minutes' => 5,
        ];

        $settings = apply_filters( 'timetics_default_settings', $settings );

        foreach ( $settings as $key => $value ) {
            timetics_update_option( $key, $value );
        }
    }
}

if ( ! function_exists( 'timetics_google_setup' ) ) {

    /**
     * Checking google auth is configured or not
     *
     * @return  bool
     */
    function timetics_google_setup() {
        $client_id     = timetics_get_option( 'google_app_client_id' );
        $client_secret = timetics_get_option( 'google_app_client_secret' );

        if ( $client_id && $client_secret ) {
            return true;
        }

        return false;
    }
}

if ( ! function_exists( 'timetics_get_post_status' ) ) {

    /**
     * Get post status
     *
     * @return  array
     */
    function timetics_get_post_status() {
        return [
            'approved',
            'pending',
            'cancel',
            'completed',
        ];
    }
}

if ( ! function_exists( 'timetics_get_payment_methods' ) ) {
    /**
     * Get payment methods
     *
     * @return array
     */
    function timetics_get_payment_methods() {
        $payment_methods = [
            [
                'name'   => esc_html__( 'Stripe', 'timetics' ),
                'status' => timetics_get_option( 'stripe_status' ),
            ],
            [
                'name'   => esc_html__( 'Local Pay', 'timetics' ),
                'status' => timetics_get_option( 'local_pay_status' ),
            ],
            [
                'name'   => esc_html__( 'Woocommerce', 'timetics' ),
                'status' => timetics_is_woocommerce_active(),
            ],
        ];

        return apply_filters( 'timetics_payment_methods', $payment_methods );
    }
}

if ( ! function_exists( 'timetics_replace_placeholder' ) ) {
    /**
     * Replace string with placeholder
     *
     * @param   array  $placeholders
     * @param   string  $text
     *
     * @return  string
     */
    function timetics_replace_placeholder( $text, $placeholders = [] ) {

        if ( ! $placeholders ) {
            return $text;
        }

        foreach ( $placeholders as $placeholder => $replace_with ) {
            $text = str_replace( $placeholder, $replace_with, $text );
        }

        return $text;
    }
}

if ( ! function_exists( 'timetics_register_cron' ) ) {
    /**
     * Register timetics cron
     *
     * @return  void
     */
    function timetics_register_cron() {
        wp_schedule_event( time(), 'daily', 'timetics_booking_clear_schedule' );

        // Guarded: Hooks::maybe_schedule_cleanup_cron() also calls this on
        // every 'init', so without the guard it'd stack duplicate events.
        if ( ! wp_next_scheduled( 'timetics_cleanup_unpaid_bookings' ) ) {
            wp_schedule_event( time(), 'timetics_five_minutes', 'timetics_cleanup_unpaid_bookings' );
        }
    }
}

if ( ! function_exists( 'timetics_is_woocommerce_active' ) ) {

    /**
     * Check woocommerce is active or not
     *
     * @return  bool
     */
    function timetics_is_woocommerce_active() {

        if ( function_exists( 'WC' ) && timetics_get_option( 'wc_integration' ) ) {
            return true;
        }

        return false;
    }
}

if ( ! function_exists( 'timetics_add_woocommerce_product_cat' ) ) {
    /**
     * Add woocommerce product category
     *
     * @return  void
     */
    function timetics_add_woocommerce_product_cat() {
        $category_name = __( 'Timetics Meeting', 'timetics' );

        // Prepare category arguments
        $category_args = array(
            'taxonomy'             => 'product_cat',
            'cat_name'             => $category_name,
            'category_description' => __( 'Timetics meeting', 'timetics' ),
            'slug'                 => 'timetics-meeting',
        );

        // Insert the category
        wp_insert_term( $category_args['cat_name'], $category_args['taxonomy'], $category_args );
    }
}

if ( ! function_exists( 'timetics_is_valid_timezone' ) ) {
    /**
     * Check a timezone is valid or not
     *
     * @param   string  $timezone
     *
     * @return  bool
     */
    function timetics_is_valid_timezone( $timezone ) {
        try {
            new DateTimeZone( $timezone );
        } catch ( Exception $e ) {
            return false;
        }

        return true;
    }
}

if ( ! function_exists( 'timetics_datetime' ) ) {

    /**
     * Timetics date time with timezone
     *
     * @param   int | string  $date
     * @param   string  $timezone
     *
     * @return  string
     */
    function timetics_datetime( $format, $date, $timezone = '' ) {
        $date = is_int( $date ) ? gmdate( 'Y-m-d g:ia', $date ) : $date;

        if ( ! $timezone ) {
            $timezone = 'UTC';
        }

        // Create a DateTime object with the provided timestamp and time zone
        $datetime = new \DateTime( $date, new \DateTimeZone( $timezone ) );

        // Get the converted timestamp
        return $datetime->format( $format );
    }
}

if ( ! function_exists( 'timetics_convert_timezone' ) ) {

    /**
     * Convert date and time fron timezone to another timezone
     *
     * @param   string  $format
     * @param   string  $date_time
     * @param   string  $from
     * @param   string  $to
     *
     * @return  \DateTime
     */
    function timetics_convert_timezone( $date_time_string, $from, $to ) {

        if ( empty( $from ) || empty( $to ) ) {
            return new DateTime( $date_time_string );
        }

        $date_time_string = is_int( $date_time_string ) ? gmdate( 'Y-m-d g:ia', $date_time_string ) : gmdate( 'Y-m-d g:ia', strtotime( $date_time_string ) );

        $datetime = new DateTime( $date_time_string, new DateTimeZone( $from ) );
        $datetime->setTimezone( new DateTimeZone( $to ) );

        return $datetime;
    }
}

if ( ! function_exists( 'timetics_generate_username' ) ) {
    /**
     * Generate unique username
     *
     * @param   string  $email
     *
     * @return  string
     */
    function timetics_generate_username( $email ) {
        $username = strtok( $email, '@' );

        if ( username_exists( $username ) ) {
            $username = $username . wp_rand( 10, 100 );
        }

        return $username;
    }
}

if ( ! function_exists( 'timetics_is_json' ) ) {
    /**
     * Check a string is valid json or not
     *
     * @param   string  $json_string
     *
     * @return  bool
     */
    function timetics_is_json( $json_string ) {
        if ( ! is_string( $json_string ) ) {
            return false;
        }

        $data = json_decode( $json_string, true );

        return $data !== null && json_last_error() === JSON_ERROR_NONE;
    }
}

if ( ! function_exists( 'timetics_is_google_meet_connected' ) ) {

    /**
     * Check google meet is connected or not
     *
     * @param   integer  $user_id
     *
     * @return  bool
     */
    function timetics_is_google_meet_connected( $user_id = 0 ) {
        if ( ! $user_id ) {
            $user_id = get_current_user_id();
        }

        $google_auth = timetics_get_google_access_token( $user_id );

        return ! empty( $google_auth );
    }
}

if ( ! function_exists( 'timetics_is_zoom_connected' ) ) {

    /**
     * Check google meet is connected or not
     *
     * @param   integer  $user_id
     *
     * @return  bool
     */
    function timetics_is_zoom_connected( $user_id = 0 ) {
        if ( ! $user_id ) {
            $user_id = get_current_user_id();
        }

        if ( ! function_exists( 'timetics_get_zoom_access_token' ) ) {
            return false;
        }

        $zoom_auth = timetics_get_zoom_access_token( $user_id );

        return ! empty( $zoom_auth );
    }
}

if ( ! function_exists( 'timetics_get_current_user' ) ) {
    /**
     * Get current user data
     *
     * @return  array
     */
    function timetics_get_current_user() {
        $user_id = get_current_user_id();
        $user    = get_userdata( $user_id );

        $user_data = [
            'id'        => $user->ID,
            'username'  => $user->user_login,
            'email'     => $user->user_email,
            'roles'     => $user-> roles,
        ];
        return $user_data;
    }
}

if ( ! function_exists( 'timetics_connected_platforms' ) ) {

    /**
     * Get connected platforms
     *
     * @param   integer  $id
     *
     * @return  array       Connected platform lists
     */
    function timetics_connected_platforms( $id ) {
        $integrations = timetics_get_staff_integrations( $id );
        $integration_names = [];

        if ( $integrations ) {
            foreach ( $integrations as $integration ) {
                if ( $integration['connected'] ) {
                    $integration_names[] = $integration['id'];
                }
            }
        }

        return $integration_names;
    }
}

if ( ! function_exists( 'timetics_modules_list' ) ) {
    /**
     * Get the master list of Arraytics plugins for the About Us page.
     *
     * Each entry describes an external Arraytics plugin that can be
     * installed/activated/deactivated from within the Timetics admin.
     *
     * @return array
     */
    function timetics_modules_list(): array {
        return [
            'eventin' => [
                'name'          => 'eventin',
                'slug'          => 'wp-event-solution',
                'type'          => 'plugin',
                'upgrade'       => false,
                'upgrade_link'  => 'https://themewinter.com/eventin',
                'status'        => 'on',
                'is_pro'        => false,
                'title'         => __( 'Eventin', 'timetics' ),
                'description'   => __( 'Best Free WordPress Event Calendar Plugin to manage booking, registration, RSVPs, schedule recurring events and sell event tickets with WooCommerce.', 'timetics' ),
                'icon'          => \Timetics\Core\Addon\Extension_Icon::get( 'eventin' ),
                'notice'        => '',
                'demo_link'     => 'https://product.themewinter.com/eventin',
                'settings_link' => '',
                'doc_link'      => 'https://support.themewinter.com/docs/plugins/docs/eventin/',
            ],
            'booktics' => [
                'name'          => 'booktics',
                'slug'          => 'booktics',
                'type'          => 'plugin',
                'upgrade'       => false,
                'upgrade_link'  => 'https://arraytics.com/booktics',
                'status'        => 'on',
                'is_pro'        => false,
                'title'         => __( 'Booktics', 'timetics' ),
                'description'   => __( 'The ultimate online booking plugin for WordPress — services, staff, schedules, and payments in one place.', 'timetics' ),
                'icon'          => \Timetics\Core\Addon\Extension_Icon::get( 'booktics' ),
                'notice'        => '',
                'demo_link'     => 'https://arraytics.com/booktics',
                'settings_link' => '',
                'doc_link'      => 'https://docs.arraytics.com/docs/booktics/',
            ],
            'poptics'  => [
                'name'          => 'poptics',
                'slug'          => 'poptics',
                'type'          => 'plugin',
                'upgrade'       => false,
                'upgrade_link'  => 'https://arraytics.com/poptics',
                'status'        => 'on',
                'is_pro'        => false,
                'title'         => __( 'Poptics', 'timetics' ),
                'description'   => __( 'Discover the ultimate popup mix with Poptics Popup Builder to boost your lead generation and sales conversions.', 'timetics' ),
                'icon'          => \Timetics\Core\Addon\Extension_Icon::get( 'poptics' ),
                'notice'        => '',
                'demo_link'     => 'https://arraytics.com/poptics',
                'settings_link' => '',
                'doc_link'      => 'https://docs.aethonic.com/docs/getting-started/intro/',
            ],
            'wpcafe'   => [
                'name'          => 'wpcafe',
                'slug'          => 'wp-cafe',
                'type'          => 'plugin',
                'upgrade'       => false,
                'upgrade_link'  => 'https://themewinter.com/wpcafe',
                'status'        => 'on',
                'is_pro'        => false,
                'title'         => __( 'WPCafe', 'timetics' ),
                'description'   => __( 'WPCafe — Restaurant plugin with Online Ordering, Delivery, Pickup, Food Menu, Reservations, and Table Management.', 'timetics' ),
                'icon'          => \Timetics\Core\Addon\Extension_Icon::get( 'wpcafe' ),
                'notice'        => '',
                'demo_link'     => 'https://product.themewinter.com/wpcafe',
                'settings_link' => '',
                'doc_link'      => 'https://support.themewinter.com/docs/plugins/docs/wp-cafe/',
            ],
            'aisentic' => [
                'name'          => 'aisentic',
                'slug'          => 'aisentic',
                'type'          => 'plugin',
                'upgrade'       => false,
                'upgrade_link'  => '',
                'status'        => 'on',
                'is_pro'        => false,
                'title'         => __( 'Aisentic', 'timetics' ),
                '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' ),
                'icon'          => \Timetics\Core\Addon\Extension_Icon::get( 'aisentic' ),
                'notice'        => '',
                'demo_link'     => '',
                'settings_link' => '',
                'doc_link'      => 'https://support.themewinter.com/docs/plugins/docs/aisentic/',
            ],
            'optiontics' => [
                'name'          => 'optiontics',
                'slug'          => 'optiontics',
                'type'          => 'plugin',
                'upgrade'       => false,
                'upgrade_link'  => '',
                'status'        => 'on',
                'is_pro'        => false,
                'title'         => __( 'Optiontics', 'timetics' ),
                'description'   => __( 'Offer paid add-ons and extra options with every booking — services, extras, packages or custom upgrades that customers can pick during checkout.', 'timetics' ),
                'icon'          => \Timetics\Core\Addon\Extension_Icon::get( 'optiontics' ),
                'notice'        => '',
                'demo_link'     => '',
                'settings_link' => '',
                'doc_link'      => 'https://support.themewinter.com/docs/plugins/plugin-docs/optiontics/how-to-create-product-options/',
                'download_url'  => 'https://github.com/themewinter/optiontics-public/releases/download/release/optiontics.zip',
            ],
        ];
    }
}

if ( ! function_exists( 'timetics_extension' ) ) {
    /**
     * Get the Timetics Extension manager instance (tools-sdk).
     *
     * @return \Arraytics\ToolsSdk\Extension
     */
    function timetics_extension(): \Arraytics\ToolsSdk\Extension {
        return new \Arraytics\ToolsSdk\Extension( 'timetics_available_modules', timetics_modules_list() );
    }
}

if ( ! function_exists( 'timetics_get_enabled_modules' ) ) {
    /**
     * Get list of enabled module slugs.
     *
     * @return array
     */
    function timetics_get_enabled_modules(): array {
        $modules = get_option( 'timetics_available_modules', [] );

        if ( empty( $modules ) || ! is_array( $modules ) ) {
            return [];
        }

        $enabled = [];

        foreach ( $modules as $slug => $data ) {
            if ( is_array( $data ) && isset( $data['status'] ) && 'on' === $data['status'] ) {
                $enabled[] = $slug;
            } elseif ( is_string( $data ) && 'on' === $data ) {
                $enabled[] = $slug;
            }
        }

        return $enabled;
    }
}

if ( ! function_exists( 'timetics_wp_timezone_string' ) ) {
    /**
     * wp_timezone_string() polyfill — core's version needs WP 5.3, plugin supports 5.2.
     *
     * @return  string
     */
    function timetics_wp_timezone_string() {
        if ( function_exists( 'wp_timezone_string' ) ) {
            return wp_timezone_string();
        }

        $timezone_string = get_option( 'timezone_string' );

        if ( $timezone_string ) {
            return $timezone_string;
        }

        $offset  = (float) get_option( 'gmt_offset' );
        $hours   = (int) $offset;
        $minutes = abs( ( $offset - $hours ) * 60 );

        return sprintf( '%s%02d:%02d', ( $offset < 0 ) ? '-' : '+', abs( $hours ), $minutes );
    }
}

if ( ! function_exists( 'timetics_reminder_fallback_timezone' ) ) {
    /**
     * Timezone to interpret a booking's stored wall clock in when the booking
     * itself carries no usable timezone.
     *
     * A booking normally stores the customer's timezone next to the customer's
     * local start time, and that pair must be read together. Bookings created
     * before that meta existed, or from the admin side, have no timezone — those
     * times were entered against the site clock, so the site timezone is the
     * closer reading. Falling back to UTC put every reminder on such a booking
     * out by the site's GMT offset.
     *
     * @return  string  A timezone identifier or ±hh:mm offset accepted by DateTimeZone.
     */
    function timetics_reminder_fallback_timezone() {
        $timezone = timetics_wp_timezone_string();

        return $timezone ? $timezone : 'UTC';
    }
}

if ( ! function_exists( 'timetics_format_email_datetime' ) ) {
    /**
     * Format date, time and day according to WordPress admin settings
     * Used in email templates to display consistent date/time formats
     *
     * @param   string  $start_date  Start date (Y-m-d format)
     * @param   string  $start_time  Start time (h:i a format)
     *
     * @return  array  Array with 'day', 'time', 'date' keys
     */
    function timetics_format_email_datetime( $start_date, $start_time = '' ) {
        $wp_date_format = get_option( 'date_format' );
        $wp_time_format = get_option( 'time_format' );

        $datetime = $start_time
            ? $start_date . ' ' . $start_time
            : $start_date;

        $timestamp = strtotime( $datetime );

        return [
            'day'  => date_i18n( 'l', $timestamp ),
            'time' => date_i18n( $wp_time_format, $timestamp ),
            'date' => date_i18n( $wp_date_format, $timestamp ),
        ];
    }
}

if ( ! function_exists( 'timetics_aisentic_identity' ) ) {
    /**
     * Resolve the identity Timetics hands to Aisentic when the user connects.
     *
     * The consent UI shows these exact values before anything leaves the site,
     * so the connect request must read them from here too. Showing one email
     * and sending another would break the consent.
     *
     * The account belongs to the person who opts in, so it is the logged-in
     * user's name and email. The Ask AI setup dialog lets them edit the email,
     * which arrives here as $email. The site admin email is the last fallback.
     *
     * @param string $email Optional email the user typed in the consent UI.
     * @return array{name:string,email:string,site_url:string}
     */
    function timetics_aisentic_identity( $email = '' ) {
        $user  = wp_get_current_user();
        $name  = sanitize_text_field( (string) $user->display_name );
        $email = sanitize_email( (string) ( $email ?: $user->user_email ?: get_option( 'admin_email', '' ) ) );

        return [
            'name'     => $name,
            'email'    => $email,
            'site_url' => site_url(),
        ];
    }
}

if ( ! function_exists( 'timetics_aisentic_is_registered' ) ) {
    /**
     * Whether Aisentic already holds a provider api key.
     *
     * Reads Aisentic's own settings, so a site the user registered from
     * Aisentic's own screens counts as connected as well. Without that the
     * dashboard banner would keep nagging people who are already set up.
     *
     * @return bool
     */
    function timetics_aisentic_is_registered() {
        if ( function_exists( 'aisentic_get_option' ) ) {
            return ! empty( aisentic_get_option( 'llmProviders.aisentic.apiKey', '' ) );
        }

        $settings = get_option( 'aisentic_settings', [] );

        return ! empty( $settings['llmProviders']['aisentic']['apiKey'] );
    }
}

```
