PluginProbe
Timetics – Appointment Booking Calendar & Scheduling / 1.0.64
Timetics – Appointment Booking Calendar & Scheduling v1.0.64
1.0.64 1.0.62 1.0.63 1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 trunk 1.0.0 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.2 1.0.20 1.0.21 All 65 releases
← All changes | utils/global-helper.php +729 -44 1.0.1 → 1.0.64 View file →
@@ -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;
@@ -22,10 +24,10 @@
22 24
23 25 if ( ! empty( $elements ) ) {
24 26 foreach ( $elements as $element ) {
25 27 $data = [
26 - 'name' => $element->getAttribute( 'value' ),
27 - 'value' => $element->textContent, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
28 + 'value' => $element->getAttribute( 'value' ),
29 + 'name' => $element->textContent, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
28 30 ];
29 31
30 32 $timezones[] = $data;
31 33 }
@@ -47,22 +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 - timetics_update_google_auth( $user_id, $data );
64 + // Needs a refresh.
65 + $refresh_token = timetics_get_google_refresh_token( $user_id );
66 +
67 + if ( empty( $refresh_token ) ) {
68 + return false;
62 69 }
63 70
64 - return $data['access_token'];
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 ) {
79 + return false;
80 + }
81 +
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;
86 + }
87 +
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'];
65 92 }
66 93 }
67 94
68 95 if ( ! function_exists( 'timetics_get_google_refresh_token' ) ) {
@@ -101,8 +128,12 @@
101 128 *
102 129 * @return void
103 130 */
104 131 function timetics_update_google_auth( $user_id = 0, $data = [] ) {
132 + if ( empty( $data ) || ! is_array( $data ) ) {
133 + return;
134 + }
135 +
105 136 if ( ! empty( $data['code'] ) ) {
106 137 update_user_meta( $user_id, 'timetics_google_auth_code', $data['code'] );
107 138 }
108 139
@@ -109,11 +140,17 @@
109 140 if ( ! empty( $data['refresh_token'] ) ) {
110 141 update_user_meta( $user_id, 'timetics_google_refresh_token', $data['refresh_token'] );
111 142 }
112 143
113 - $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;
114 150
115 151 update_user_meta( $user_id, 'timetics_google_auth', $data );
152 + delete_user_meta( $user_id, 'timetics_google_auth_error' );
116 153 }
117 154 }
118 155
119 156 if ( ! function_exists( 'timetics_get_google_auth' ) ) {
@@ -162,11 +199,11 @@
162 199 *
163 200 * @return string
164 201 */
165 202 function timetics_get_google_auth_url() {
166 - $client = timetics_get_google_client();
203 + $local_url = site_url( 'timetics-integration/google-auth' );
167 204
168 - return $client->get_auth_url();
205 + return wp_nonce_url( $local_url, 'timetics_auth_action', 'timetics_auth_nonce' );
169 206 }
170 207 }
171 208
172 209 if ( ! function_exists( 'timetics_get_posts' ) ) {
@@ -289,10 +326,12 @@
289 326 * @return integer
290 327 */
291 328 function timetics_get_total_sale( $args = [] ) {
292 329 $defaults = [
293 - 'post_type' => 'timetics-booking',
330 + 'post_type' => 'timetics-booking',
331 + 'post_status' => 'approved',
294 332 ];
333 +
295 334 $args = wp_parse_args( $args, $defaults );
296 335
297 336 $posts = timetics_get_posts( $args );
298 337
@@ -299,9 +338,9 @@
299 338 $total = 0;
300 339
301 340 foreach ( $posts->posts as $post ) {
302 341 $booking = new Booking( $post->ID );
303 - $total += $booking->get_total();
342 + $total += floatval( $booking->get_total() );
304 343 }
305 344
306 345 return $total;
307 346 }
@@ -306,8 +345,70 @@
306 345 return $total;
307 346 }
308 347 }
309 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 +
310 411 if ( ! function_exists( 'timetics_get_staff_integrations' ) ) {
311 412 /**
312 413 * Get all staff integrations
313 414 *
@@ -313,33 +414,43 @@
313 414 *
314 415 * @return array
315 416 */
316 417 function timetics_get_staff_integrations( $user_id = 0 ) {
317 - $google_auth = get_user_meta( $user_id, 'timetics_google_auth_code', true );
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 +
318 425 $google_credentials = timetics_get_google_credentials();
319 426 $is_setup = ! empty( $google_credentials['client_id'] ) && ! empty( $google_credentials['client_secret'] );
320 - $current_user_id = ($user_id == get_current_user_id()) ? true : false;
427 + $current_user_id = ( $user_id == get_current_user_id() ) ? true : false;
321 428
429 + $integrations = [
430 + [
431 + 'id' => 'google-calendar-meet',
432 + 'name' => esc_html__( 'Google Meet', 'timetics' ),
433 + 'description' => esc_html__( 'Connect your Meet to sync your booked events.', 'timetics' ),
434 + 'auth_url' => timetics_get_google_auth_url(),
435 + 'connected' => $is_connected,
436 + 'needs_reauth' => $needs_reauth,
437 + 'setup' => $is_setup,
438 + 'current_user' => $current_user_id,
439 + ],
440 + [
441 + 'id' => 'google-calendar',
442 + 'name' => esc_html__( 'Google Calendar', 'timetics' ),
443 + 'description' => esc_html__( 'Connect your Meet to sync your booked events.', 'timetics' ),
444 + 'auth_url' => timetics_get_google_auth_url(),
445 + 'connected' => $is_connected,
446 + 'needs_reauth' => $needs_reauth,
447 + 'setup' => $is_setup,
448 + 'current_user' => $current_user_id,
449 + ],
450 + ];
322 451
323 - $integrations = [];
324 - $integrations[] = [
325 - 'id' => 'google-calendar-meet',
326 - 'name' => __( 'Google Meet', 'timetics' ),
327 - 'description' => __('Connect your Meet to sync your booked events.', 'timetics'),
328 - 'auth_url' => timetics_get_google_auth_url(),
329 - 'connected' => ! empty( $google_auth ),
330 - 'setup' => $is_setup,
331 - 'current_user' => $current_user_id,
332 - ];
333 - $integrations[] = [
334 - 'id' => 'google-calendar',
335 - 'name' => __( 'Google Calendar', 'timetics' ),
336 - 'description' => __('Connect your Meet to sync your booked events.', 'timetics'),
337 - 'auth_url' => timetics_get_google_auth_url(),
338 - 'connected' => ! empty( $google_auth ),
339 - 'setup' => $is_setup,
340 - 'current_user' => $current_user_id,
341 - ];
452 + $integrations = apply_filters( 'timetics_staff_integrations', $integrations, $user_id );
342 453
343 454 return $integrations;
344 455 }
345 456 }
@@ -367,17 +478,9 @@
367 478 *
368 479 * @return string
369 480 */
370 481 function timetics_get_default_timezone() {
371 - $timezone = date_default_timezone_get();
372 - $offset = get_option( 'gmt_offset' );
373 -
374 - if ( $offset < 0 ) {
375 - $timezone = $timezone . $offset;
376 - } else {
377 - $timezone = $timezone . '+' . $offset;
378 - }
379 -
482 + $timezone = get_option( 'timezone_string' );
380 483 return $timezone;
381 484 }
382 485 }
383 486
@@ -403,12 +506,18 @@
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',
513 + 'primary_color' => '#3161F1',
514 + 'secondary_color' => '#6188ff',
515 + 'unpaid_booking_expiry_minutes' => 5,
409 516 ];
410 517
518 + $settings = apply_filters( 'timetics_default_settings', $settings );
519 +
411 520 foreach ( $settings as $key => $value ) {
412 521 timetics_update_option( $key, $value );
413 522 }
414 523 }
@@ -446,6 +555,582 @@
446 555 'pending',
447 556 'cancel',
448 557 'completed',
449 558 ];
559 + }
560 +}
561 +
562 +if ( ! function_exists( 'timetics_get_payment_methods' ) ) {
563 + /**
564 + * Get payment methods
565 + *
566 + * @return array
567 + */
568 + function timetics_get_payment_methods() {
569 + $payment_methods = [
570 + [
571 + 'name' => esc_html__( 'Stripe', 'timetics' ),
572 + 'status' => timetics_get_option( 'stripe_status' ),
573 + ],
574 + [
575 + 'name' => esc_html__( 'Local Pay', 'timetics' ),
576 + 'status' => timetics_get_option( 'local_pay_status' ),
577 + ],
578 + [
579 + 'name' => esc_html__( 'Woocommerce', 'timetics' ),
580 + 'status' => timetics_is_woocommerce_active(),
581 + ],
582 + ];
583 +
584 + return apply_filters( 'timetics_payment_methods', $payment_methods );
585 + }
586 +}
587 +
588 +if ( ! function_exists( 'timetics_replace_placeholder' ) ) {
589 + /**
590 + * Replace string with placeholder
591 + *
592 + * @param array $placeholders
593 + * @param string $text
594 + *
595 + * @return string
596 + */
597 + function timetics_replace_placeholder( $text, $placeholders = [] ) {
598 +
599 + if ( ! $placeholders ) {
600 + return $text;
601 + }
602 +
603 + foreach ( $placeholders as $placeholder => $replace_with ) {
604 + $text = str_replace( $placeholder, $replace_with, $text );
605 + }
606 +
607 + return $text;
608 + }
609 +}
610 +
611 +if ( ! function_exists( 'timetics_register_cron' ) ) {
612 + /**
613 + * Register timetics cron
614 + *
615 + * @return void
616 + */
617 + function timetics_register_cron() {
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 + }
625 + }
626 +}
627 +
628 +if ( ! function_exists( 'timetics_is_woocommerce_active' ) ) {
629 +
630 + /**
631 + * Check woocommerce is active or not
632 + *
633 + * @return bool
634 + */
635 + function timetics_is_woocommerce_active() {
636 +
637 + if ( function_exists( 'WC' ) && timetics_get_option( 'wc_integration' ) ) {
638 + return true;
639 + }
640 +
641 + return false;
642 + }
643 +}
644 +
645 +if ( ! function_exists( 'timetics_add_woocommerce_product_cat' ) ) {
646 + /**
647 + * Add woocommerce product category
648 + *
649 + * @return void
650 + */
651 + function timetics_add_woocommerce_product_cat() {
652 + $category_name = __( 'Timetics Meeting', 'timetics' );
653 +
654 + // Prepare category arguments
655 + $category_args = array(
656 + 'taxonomy' => 'product_cat',
657 + 'cat_name' => $category_name,
658 + 'category_description' => __( 'Timetics meeting', 'timetics' ),
659 + 'slug' => 'timetics-meeting',
660 + );
661 +
662 + // Insert the category
663 + wp_insert_term( $category_args['cat_name'], $category_args['taxonomy'], $category_args );
664 + }
665 +}
666 +
667 +if ( ! function_exists( 'timetics_is_valid_timezone' ) ) {
668 + /**
669 + * Check a timezone is valid or not
670 + *
671 + * @param string $timezone
672 + *
673 + * @return bool
674 + */
675 + function timetics_is_valid_timezone( $timezone ) {
676 + try {
677 + new DateTimeZone( $timezone );
678 + } catch ( Exception $e ) {
679 + return false;
680 + }
681 +
682 + return true;
683 + }
684 +}
685 +
686 +if ( ! function_exists( 'timetics_datetime' ) ) {
687 +
688 + /**
689 + * Timetics date time with timezone
690 + *
691 + * @param int | string $date
692 + * @param string $timezone
693 + *
694 + * @return string
695 + */
696 + function timetics_datetime( $format, $date, $timezone = '' ) {
697 + $date = is_int( $date ) ? gmdate( 'Y-m-d g:ia', $date ) : $date;
698 +
699 + if ( ! $timezone ) {
700 + $timezone = 'UTC';
701 + }
702 +
703 + // Create a DateTime object with the provided timestamp and time zone
704 + $datetime = new \DateTime( $date, new \DateTimeZone( $timezone ) );
705 +
706 + // Get the converted timestamp
707 + return $datetime->format( $format );
708 + }
709 +}
710 +
711 +if ( ! function_exists( 'timetics_convert_timezone' ) ) {
712 +
713 + /**
714 + * Convert date and time fron timezone to another timezone
715 + *
716 + * @param string $format
717 + * @param string $date_time
718 + * @param string $from
719 + * @param string $to
720 + *
721 + * @return \DateTime
722 + */
723 + function timetics_convert_timezone( $date_time_string, $from, $to ) {
724 +
725 + if ( empty( $from ) || empty( $to ) ) {
726 + return new DateTime( $date_time_string );
727 + }
728 +
729 + $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 ) );
730 +
731 + $datetime = new DateTime( $date_time_string, new DateTimeZone( $from ) );
732 + $datetime->setTimezone( new DateTimeZone( $to ) );
733 +
734 + return $datetime;
735 + }
736 +}
737 +
738 +if ( ! function_exists( 'timetics_generate_username' ) ) {
739 + /**
740 + * Generate unique username
741 + *
742 + * @param string $email
743 + *
744 + * @return string
745 + */
746 + function timetics_generate_username( $email ) {
747 + $username = strtok( $email, '@' );
748 +
749 + if ( username_exists( $username ) ) {
750 + $username = $username . wp_rand( 10, 100 );
751 + }
752 +
753 + return $username;
754 + }
755 +}
756 +
757 +if ( ! function_exists( 'timetics_is_json' ) ) {
758 + /**
759 + * Check a string is valid json or not
760 + *
761 + * @param string $json_string
762 + *
763 + * @return bool
764 + */
765 + function timetics_is_json( $json_string ) {
766 + if ( ! is_string( $json_string ) ) {
767 + return false;
768 + }
769 +
770 + $data = json_decode( $json_string, true );
771 +
772 + return $data !== null && json_last_error() === JSON_ERROR_NONE;
773 + }
774 +}
775 +
776 +if ( ! function_exists( 'timetics_is_google_meet_connected' ) ) {
777 +
778 + /**
779 + * Check google meet is connected or not
780 + *
781 + * @param integer $user_id
782 + *
783 + * @return bool
784 + */
785 + function timetics_is_google_meet_connected( $user_id = 0 ) {
786 + if ( ! $user_id ) {
787 + $user_id = get_current_user_id();
788 + }
789 +
790 + $google_auth = timetics_get_google_access_token( $user_id );
791 +
792 + return ! empty( $google_auth );
793 + }
794 +}
795 +
796 +if ( ! function_exists( 'timetics_is_zoom_connected' ) ) {
797 +
798 + /**
799 + * Check google meet is connected or not
800 + *
801 + * @param integer $user_id
802 + *
803 + * @return bool
804 + */
805 + function timetics_is_zoom_connected( $user_id = 0 ) {
806 + if ( ! $user_id ) {
807 + $user_id = get_current_user_id();
808 + }
809 +
810 + if ( ! function_exists( 'timetics_get_zoom_access_token' ) ) {
811 + return false;
812 + }
813 +
814 + $zoom_auth = timetics_get_zoom_access_token( $user_id );
815 +
816 + return ! empty( $zoom_auth );
817 + }
818 +}
819 +
820 +if ( ! function_exists( 'timetics_get_current_user' ) ) {
821 + /**
822 + * Get current user data
823 + *
824 + * @return array
825 + */
826 + function timetics_get_current_user() {
827 + $user_id = get_current_user_id();
828 + $user = get_userdata( $user_id );
829 +
830 + $user_data = [
831 + 'id' => $user->ID,
832 + 'username' => $user->user_login,
833 + 'email' => $user->user_email,
834 + 'roles' => $user-> roles,
835 + ];
836 + return $user_data;
837 + }
838 +}
839 +
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;
862 + }
863 +}
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'] );
450 1135 }
451 1136 }