prefix . 'vizyt_'; } /** * Currencies a service can be priced in (ISO 4217). * * @return string[] */ function vizyt_currencies(): array { $currencies = apply_filters( 'vizyt_currencies', [ 'UAH', 'USD', 'EUR', 'CHF', 'GBP', 'PLN', 'CZK' ] ); return array_values( array_unique( array_filter( array_map( 'strtoupper', (array) $currencies ), function ( $c ) { return (bool) preg_match( '/^[A-Z]{3}$/', $c ); } ) ) ); } /** * Translate a user-entered string (service, category, staff, custom field…) * through Polylang / WPML. Returns $value unchanged without a multilingual * plugin or translation. * * @param string $value Original (default-language) text. * @param string $name Stable string name, e.g. "service_12_name". * @param string|null $lang Language slug; current language when null. */ function vizyt_translate( string $value, string $name, ?string $lang = null ): string { return \Vizyt\Integrations\Multilingual::translate( $value, $name, $lang ); } /** * Register a user-entered string for translation (Polylang strings / * WPML String Translation, group "Vizyt"). */ function vizyt_register_string( string $name, string $value, bool $multiline = false ): void { \Vizyt\Integrations\Multilingual::register( $name, $value, $multiline ); } /** * Public data of an active service, translated to the current language. * Handy in themes (e.g. with the ACF "Vizyt service" field). * * @return array{id:int,name:string,description:string,duration:int,price:float,currency:string,category_id:int,shortcode:string}|null */ function vizyt_get_service( int $id ): ?array { foreach ( \Vizyt\Models\Service::getPublicList() as $row ) { if ( (int) $row->id === $id ) { $row = \Vizyt\Integrations\Multilingual::translate_service( $row ); return [ 'id' => (int) $row->id, 'name' => (string) $row->name, 'description' => (string) $row->description, 'duration' => (int) $row->duration, 'price' => (float) $row->price, 'currency' => (string) $row->currency, 'category_id' => (int) $row->category_id, 'shortcode' => '[vizyt_booking service_id="' . (int) $row->id . '"]', ]; } } return null; } /** * Active services for themes, translated to the current language. * * @param array{category?: int|string} $args category: ID or slug. * @return array */ function vizyt_get_services( array $args = [] ): array { global $wpdb; $category_id = null; if ( isset( $args['category'] ) && '' !== $args['category'] ) { $category_id = is_numeric( $args['category'] ) ? (int) $args['category'] : (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}vizyt_categories WHERE slug = %s", (string) $args['category'] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Vizyt's own categories table, read fresh. if ( ! $category_id ) { return []; } } $out = []; foreach ( \Vizyt\Integrations\Multilingual::translate_services( \Vizyt\Models\Service::getPublicList( $category_id ) ) as $row ) { $out[] = [ 'id' => (int) $row->id, 'name' => (string) $row->name, 'description' => (string) $row->description, 'duration' => (int) $row->duration, 'price' => (float) $row->price, 'currency' => (string) $row->currency, 'image_url' => (string) $row->image_url, 'category_id' => (int) $row->category_id, 'category_name' => (string) $row->category_name, 'shortcode' => '[vizyt_booking service_id="' . (int) $row->id . '"]', ]; } return $out; } /** * Active staff for themes (team sections), translated to the current language. * * @return array */ function vizyt_get_staff(): array { global $wpdb; $rows = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}vizyt_staff WHERE status = 'active' ORDER BY sort_order ASC, id ASC" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Vizyt's own staff table, read fresh. $out = []; foreach ( (array) $rows as $row ) { $out[] = [ 'id' => (int) $row->id, 'name' => vizyt_translate( (string) $row->name, 'staff_' . $row->id . '_name' ), 'job_title' => vizyt_translate( (string) ( $row->job_title ?? '' ), 'staff_' . $row->id . '_job_title' ), 'description' => vizyt_translate( (string) $row->description, 'staff_' . $row->id . '_description' ), 'avatar_url' => (string) $row->avatar_url, ]; } return $out; } /** * Price formatted for the current locale ("CHF 150.–" style is the * theme's business; this gives "CHF 150" / "150 CHF" via Intl when present). */ function vizyt_format_price( float $price, string $currency ): string { if ( class_exists( 'NumberFormatter' ) ) { $fmt = new \NumberFormatter( determine_locale(), \NumberFormatter::CURRENCY ); $fmt->setAttribute( \NumberFormatter::FRACTION_DIGITS, floor( $price ) == $price ? 0 : 2 ); // phpcs:ignore Universal.Operators.StrictComparisons -- float compared with its floor: whole prices show no decimals. $out = $fmt->formatCurrency( $price, $currency ); if ( false !== $out ) { return $out; } } return $currency . ' ' . number_format_i18n( $price, floor( $price ) == $price ? 0 : 2 ); // phpcs:ignore Universal.Operators.StrictComparisons -- float compared with its floor: whole prices show no decimals. } /** * The business timezone (Vizyt → Settings → Timezone, else the WordPress * timezone). Booking start/end times are stored as wall-clock time in this * zone, so every "now" comparison and conversion must use it — not UTC. */ function vizyt_timezone(): \DateTimeZone { $name = \Vizyt\Admin\Settings::get_timezone(); // WordPress manual offsets ("UTC+3", "UTC-9.5") are not PHP identifiers. if ( preg_match( '/^UTC([+-])(\d+)(?:\.(5|75))?$/', $name, $m ) ) { $minutes = isset( $m[3] ) ? (int) round( (float) ( '0.' . $m[3] ) * 60 ) : 0; $name = sprintf( '%s%02d:%02d', $m[1], (int) $m[2], $minutes ); } try { return new \DateTimeZone( $name ); } catch ( \Exception $e ) { return wp_timezone(); } } /** * Where "What's in Pro" links point (filterable for agencies). */ function vizyt_pro_url(): string { return (string) apply_filters( 'vizyt_pro_url', 'https://tolokastudio.com/vizyt/' ); } /** * Where "Upgrade to Pro" leads (later the checkout, through Freemius); * the Pro information page until something filters it. */ function vizyt_pro_upgrade_url(): string { return (string) apply_filters( 'vizyt_pro_upgrade_url', vizyt_pro_url() ); } /** * Price of the Pro add-on card, e.g. "€49". Empty — and no price line — unless * a filter (later: Freemius) provides one: the price is never hard-coded. */ function vizyt_pro_price(): string { return trim( (string) apply_filters( 'vizyt_pro_price', '' ) ); } /** * Plugin documentation (admin footer). */ function vizyt_docs_url(): string { return 'https://tolokastudio.com/vizyt/docs/'; } /** * Support forum on wordpress.org (admin footer). */ function vizyt_support_url(): string { return 'https://wordpress.org/support/plugin/vizyt/'; } /** * The customer's "Manage booking" page for a booking token * (home_url('/?vizyt_booking=…')). $view 'cancel' opens the cancel * confirmation, 'reschedule' the new-time picker. The token cancels and * moves the booking: never put this URL in a calendar file or a public event. */ function vizyt_manage_url( string $token, string $view = '' ): string { $args = [ 'vizyt_booking' => $token ]; if ( in_array( $view, [ 'cancel', 'reschedule' ], true ) ) { $args['vizyt_view'] = $view; } return add_query_arg( $args, home_url( '/' ) ); }