dashboard_manager = $dashboard_manager ?: new DashboardManager(); $this->business_manager = $business_manager ?: new BusinessManager(); $this->bookings_manager = $bookings_manager ?: new BookingsManager(); $this->calendar_manager = $calendar_manager ?: new CalendarManager(); $this->customers_manager = $customers_manager ?: new CustomersManager(); $this->form_manager = $form_manager ?: new FormManager(); $this->communication_manager = $communication_manager ?: new CommunicationManager(); $this->payment_methods_manager = $payment_methods_manager ?: new PaymentMethodsManager(); $this->data_tools_manager = $data_tools_manager ?: new DataToolsManager(); $this->statuses = new StatusesRepository(); $this->email_service = new EmailService(); } /** * Free admin modules. * * @return array> */ public function modules(): array { $modules = array( 'today' => array( 'title' => __( 'Today', 'abientot' ), 'menu_title' => __( 'Today', 'abientot' ), 'capability' => 'abientot_manage_bookings', 'description' => __( 'Your appointments for the day.', 'abientot' ) ), 'dashboard' => array( 'title' => __( 'Reports', 'abientot' ), 'menu_title' => __( 'Reports', 'abientot' ), 'capability' => 'abientot_view_reports', 'description' => __( 'Review revenue, bookings, status health, upcoming appointments, and setup progress.', 'abientot' ), ), 'bookings' => array( 'title' => __( 'Bookings', 'abientot' ), 'menu_title' => __( 'Bookings', 'abientot' ), 'capability' => 'abientot_manage_bookings', 'description' => __( 'Manage bookings, customers, statuses, notes, and CSV exports.', 'abientot' ), ), 'calendar' => array( 'title' => __( 'Calendar', 'abientot' ), 'menu_title' => __( 'Calendar', 'abientot' ), 'capability' => 'abientot_manage_bookings', 'description' => __( 'Plan appointments with day, week, and month views.', 'abientot' ), ), 'customers' => array( 'title' => __( 'Customers', 'abientot' ), 'menu_title' => __( 'Customers', 'abientot' ), 'capability' => 'abientot_manage_customers', 'description' => __( 'Review customer records, booking history, customer stats, and duplicate warnings.', 'abientot' ), ), 'services' => array( 'title' => __( 'Services', 'abientot' ), 'menu_title' => __( 'Services', 'abientot' ), 'capability' => 'abientot_manage_services', 'description' => __( 'Manage services, categories, images, pricing, buffers, visibility, and ordering.', 'abientot' ), ), 'staff' => array( 'title' => __( 'Team', 'abientot' ), 'menu_title' => __( 'Team', 'abientot' ), 'capability' => 'abientot_manage_staff', 'description' => __( 'Manage the free plugin staff profile, weekly schedule, breaks, and exceptions.', 'abientot' ), ), 'locations' => array( 'title' => __( 'Locations', 'abientot' ), 'menu_title' => __( 'Locations', 'abientot' ), 'capability' => 'abientot_manage_locations', 'description' => __( 'Manage the free plugin location profile.', 'abientot' ), ), 'form' => array( 'title' => __( 'Booking form', 'abientot' ), 'menu_title' => __( 'Booking form', 'abientot' ), 'capability' => 'abientot_manage_settings', 'description' => __( 'Configure and publish the public booking form.', 'abientot' ), ), 'payments' => array( 'title' => __( 'Payment Methods', 'abientot' ), 'menu_title' => __( 'Payment Methods', 'abientot' ), 'capability' => 'abientot_manage_settings', 'description' => __( 'Configure pay-on-site instructions and default manual payment status.', 'abientot' ), ), 'settings' => array( 'title' => __( 'Settings', 'abientot' ), 'menu_title' => __( 'Settings', 'abientot' ), 'capability' => 'abientot_manage_settings', 'description' => __( 'Configure booking, customer, communication, form, privacy, and advanced settings.', 'abientot' ), ), ); /** * Filters free admin modules. Future add-ons may register additional pages here. * * @param array> $modules Admin modules. */ return apply_filters( 'abientot_register_admin_modules', $modules ); } /** * Renders a shell page for future module phases. * * @param string $module Module slug. */ public function render_module( string $module ): void { $modules = $this->modules(); $this->redirect_settings_module( $module ); if ( empty( $modules[ $module ] ) || ! current_user_can( $modules[ $module ]['capability'] ) ) { wp_die( esc_html__( 'You do not have permission to access this Abientot page.', 'abientot' ) ); } $this->render_admin_app( $module ); } /** Run before WordPress admin headers so legacy destinations actually redirect. */ public function redirect_settings_module( ?string $module = null ): void { if ( null === $module ) { $page = \Abientot\Support\Request::page(); if ( 0 !== strpos( $page, 'abientot-' ) ) { return; } $module = substr( $page, 9 ); } $modules = $this->modules(); $settings_tab = $this->settings_module_tab( $module ); if ( $settings_tab !== null && isset( $modules[ $module ] ) && current_user_can( $modules[ $module ]['capability'] ) ) { $query = array( 'page' => 'abientot-settings', 'tab' => $settings_tab ); // Carry the legacy page's navigation and result parameters to the settings tab unchanged. foreach ( array( 'calendar_page', 'sms_page', 'payments_page', 'reminder_page', 'notification_page' ) as $key ) { $value = \Abientot\Support\Request::view( $key, 'int' ); if ( null !== $value ) { $query[ $key ] = max( 1, (int) $value ); } } foreach ( array( 'sms_provider', 'sms_result', 'reminder_result', 'payment_notice', 'stripe_setup_notice', 'abientot_notice', RedirectNotice::NONCE_FIELD ) as $key ) { $value = \Abientot\Support\Request::view( $key, 'key' ); if ( null !== $value ) { $query[ $key ] = $value; } } wp_safe_redirect( add_query_arg( $query, admin_url( 'admin.php' ) ) ); exit; } } /** * Renders the complete Abientot admin app shell with all permitted modules preloaded. */ private function render_admin_app( string $active ): void { $modules = $this->modules(); if ( empty( $modules[ $active ] ) || ! current_user_can( $modules[ $active ]['capability'] ) ) { $active = $this->first_accessible_module( $modules ); } if ( '' === $active ) { wp_die( esc_html__( 'You do not have permission to access Abientot.', 'abientot' ) ); } $this->open_page( $modules[ $active ]['title'] ); echo '
'; $this->render_navigation( $active ); echo '
'; $this->render_settings_notices(); foreach ( $modules as $slug => $module ) { if ( $this->settings_module_tab( $slug ) !== null ) { continue; } if ( ! current_user_can( $module['capability'] ) ) { continue; } // Reports run many aggregate queries; render them only when opened (links then load the page). if ( 'dashboard' === $slug && $active !== $slug ) { continue; } printf( '
', esc_attr( $slug ), esc_attr( $module['title'] ), $active === $slug ? '' : ' hidden' ); $this->render_with_query_notice( $active === $slug, function () use ( $slug ): void { ProBadge::context( $slug ); $this->render_module_content( $slug ); } ); echo '
'; } echo '
'; $this->close_page(); } /** * @param array> $modules Admin modules. */ private function first_accessible_module( array $modules ): string { foreach ( $modules as $slug => $module ) { if ( current_user_can( $module['capability'] ) ) { return (string) $slug; } } return ''; } private function render_module_content( string $module ): void { $modules = $this->modules(); if ( 'today' === $module ) { ( new TodayManager() )->render(); return; } if ( 'dashboard' === $module ) { $this->dashboard_manager->render_dashboard(); return; } if ( 'services' === $module ) { $this->business_manager->render_services(); return; } if ( 'bookings' === $module ) { $this->bookings_manager->render_bookings(); return; } if ( 'calendar' === $module ) { $this->calendar_manager->render_calendar(); return; } if ( 'customers' === $module ) { $this->customers_manager->render_customers(); return; } if ( 'form' === $module ) { $this->form_manager->render_form_admin(); return; } if ( 'payments' === $module ) { $this->payment_methods_manager->render_payment_methods(); return; } if ( 'staff' === $module ) { if ( ! apply_filters( 'abientot_render_staff_admin', false ) ) { $this->business_manager->render_staff(); } return; } if ( 'locations' === $module ) { $this->business_manager->render_locations(); return; } if ( 'settings' === $module ) { $this->render_settings_content(); return; } if ( apply_filters( 'abientot_render_admin_module', false, $module ) ) { return; } ?>

settings_tabs(); $current_tab = (string) ( \Abientot\Support\Request::view( 'tab', 'key' ) ?? 'general' ); $current_tab = $this->settings_tab_alias( $current_tab ); if ( empty( $tabs[ $current_tab ] ) ) { $current_tab = 'general'; } $this->render_admin_app( 'settings' ); } /** * Renders the settings module content inside the preloaded admin shell. */ private function render_settings_content(): void { $tabs = $this->settings_tabs(); $external_tabs = apply_filters( 'abientot_settings_external_tabs', array() ); $current_tab = (string) ( \Abientot\Support\Request::view( 'tab', 'key' ) ?? 'general' ); $current_tab = $this->settings_tab_alias( $current_tab ); if ( empty( $tabs[ $current_tab ] ) ) { $current_tab = 'general'; } ?> render_settings_form( array_diff_key( $tabs, $external_tabs ), $current_tab ); } ?> deferred_email_test_modals = array(); ?>
$label ) : ?>
>

render_tab_fields( $tab, $settings ); ?>
>
deferred_email_test_modals as $modal ) { $this->email_test_modal( $modal[0], $modal[1] ); } ?>
> render_with_query_notice( 'privacy-data' === $current_tab, function (): void { $this->data_tools_manager->render_privacy_tools(); } ); ?>
> render_with_query_notice( 'advanced' === $current_tab, function (): void { $this->data_tools_manager->render_system_tools(); } ); ?>
render_grouped_settings_tools( $current_tab ); ?> $settings Current settings. */ private function render_tab_fields( string $tab, array $settings ): void { switch ( $tab ) { case 'booking-rules': $this->select_field( 'default_booking_status', __( 'Booking approval', 'abientot' ), $settings, array( 'confirmed' => __( 'Confirm bookings automatically', 'abientot' ), 'pending' => __( 'Require manual approval', 'abientot' ) ), __( 'Automatic bookings are confirmed as soon as they are submitted. Manual approval keeps new bookings pending and sends the enabled Booking confirmed email when you confirm them.', 'abientot' ) ); $this->number_field( 'booking_min_notice_hours', __( 'Minimum notice', 'abientot' ), $settings, 0, 8760, __( 'Hours before an appointment must start.', 'abientot' ) ); $this->number_field( 'booking_window_days', __( 'Booking window', 'abientot' ), $settings, 1, 730, __( 'How many days ahead customers may book.', 'abientot' ) ); $this->number_field( 'booking_slot_interval_minutes', __( 'Slot interval', 'abientot' ), $settings, 5, 240, __( 'Duration of each booking slot in minutes.', 'abientot' ) ); break; case 'statuses': $this->status_label_fields(); break; case 'customer-identity': $this->checkbox_field( 'match_customer_by_email', __( 'Match by email', 'abientot' ), $settings ); $this->checkbox_field( 'match_customer_by_phone', __( 'Match by phone', 'abientot' ), $settings ); break; case 'cancellation-rescheduling': if ( has_action( 'abientot_portal_settings_fields' ) ) { do_action( 'abientot_portal_settings_fields', $settings ); } else { echo '

' . esc_html__( 'Customers can contact your business to cancel or reschedule. Keep the phone and email on your Location profile up to date, and include the cancellation and rescheduling contact variables in your customer emails.', 'abientot' ) . '

'; } break; case 'payments': case 'notifications': case 'integrations': break; case 'communication': echo '

' . esc_html__( 'Configure the sender once, then open an email below to edit its subject, HTML design, shortcodes, preview, and test delivery.', 'abientot' ) . '

'; $this->email_field( 'admin_notification_email', __( 'Admin notification email', 'abientot' ), $settings ); $this->text_field( 'email_from_name', __( 'Email from name', 'abientot' ), $settings ); $this->email_field( 'email_from_address', __( 'Email from address', 'abientot' ), $settings ); $this->email_field( 'email_reply_to_address', __( 'Reply-to address', 'abientot' ), $settings ); $this->checkbox_field( 'email_show_credit', __( 'Credit in emails', 'abientot' ), $settings, __( 'Adds a small “Powered by Abientot” line at the bottom of the emails Abientot sends.', 'abientot' ) ); $this->render_email_template_fields( $settings ); echo ''; do_action( 'abientot_communication_tools' ); echo ''; break; case 'privacy-data': echo '

' . esc_html__( 'This technical helper is not a complete privacy policy. Identify your controller, purposes and lawful bases, retention, recipients and transfers, rights and complaint authority in your published notice.', 'abientot' ) . '

' . esc_html__( 'Review sensitive data and information about minors before collecting them. Booking messages are operational templates; marketing needs its own lawful basis and choices.', 'abientot' ) . '

' . esc_html__( 'Assess each external provider and configure your consent manager where required. Opening a map link is not a substitute for provider-specific safeguards.', 'abientot' ) . '

'; $this->text_field( 'privacy_controller_name', __( 'Controller legal name', 'abientot' ), $settings ); $this->text_field( 'privacy_contact', __( 'Privacy and rights contact', 'abientot' ), $settings ); $this->text_field( 'privacy_policy_url', __( 'Published privacy policy URL', 'abientot' ), $settings ); $this->textarea_field( 'privacy_retention_statement', __( 'Retention period or criteria', 'abientot' ), $settings ); $this->textarea_field( 'privacy_recipients', __( 'Recipients and external providers', 'abientot' ), $settings ); echo ''; wp_editor( (string) $settings['booking_terms'], 'abientot_booking_terms', array( 'textarea_name' => Settings::OPTION_NAME . '[booking_terms]', 'textarea_rows' => 12, 'media_buttons' => false ) ); echo '

' . esc_html__( 'Customers can read these terms from the booking form. Set the required agreement and its label under Form → Consent & policies.', 'abientot' ) . '

'; $this->checkbox_field( 'delete_data_on_uninstall', __( 'Delete data on uninstall', 'abientot' ), $settings, __( 'Only enable this when you want uninstall to permanently remove Abientot data.', 'abientot' ) ); break; case 'advanced': $this->checkbox_field( 'debug_mode', __( 'Debug mode', 'abientot' ), $settings ); break; default: $this->select_field( 'plugin_language', __( 'Language', 'abientot' ), $settings, Settings::language_choices(), __( 'Use the WordPress language or override it for this plugin. Changing this does not replace saved email templates.', 'abientot' ) ); $this->text_field( 'business_name', __( 'Business name', 'abientot' ), $settings ); $name_choices = Settings::header_name_choices(); $this->select_field( 'header_name_font', __( 'Business name font', 'abientot' ), $settings, $name_choices['font'], __( 'How your business name appears in the Abientot header.', 'abientot' ) ); $this->select_field( 'header_name_style', __( 'Business name style', 'abientot' ), $settings, $name_choices['style'] ); $this->select_field( 'header_name_size', __( 'Business name size', 'abientot' ), $settings, $name_choices['size'] ); $this->color_field( 'header_name_color', __( 'Business name color', 'abientot' ), $settings ); $this->timezone_field( 'business_timezone', __( 'Business timezone', 'abientot' ), $settings ); $this->date_format_field( $settings ); $this->time_format_field( $settings ); $this->currency_field( 'currency', __( 'Currency', 'abientot' ), $settings ); foreach ( array( 'booking-rules' => __( 'Booking Rules', 'abientot' ), 'statuses' => __( 'Statuses', 'abientot' ), 'customer-identity' => __( 'Customer Identity', 'abientot' ), 'cancellation-rescheduling' => __( 'Cancellation & Rescheduling', 'abientot' ) ) as $section => $heading ) { echo '

' . esc_html( $heading ) . '

'; $this->render_tab_fields( $section, $settings ); } } } private function settings_tab_alias( string $tab ): string { return array( 'extensions' => 'pro', 'booking-rules' => 'general', 'statuses' => 'general', 'customer-identity' => 'general', 'cancellation-rescheduling' => 'general', 'online-payments' => 'payments', 'reminders' => 'communication', 'sms' => 'communication', 'notification-settings' => 'notifications' )[ $tab ] ?? $tab; } private function settings_module_tab(string $module):?string { return array( 'payments' => 'payments', 'online-payments' => 'payments', 'reminders' => 'communication', 'sms' => 'communication', 'notification-settings' => 'notifications', 'notifications' => 'integrations', 'calendars' => 'integrations', 'pro-setup' => 'advanced' )[ $module ] ?? null; } private function settings_tab_icon( string $tab ): string { return array( 'general' => 'dashicons-admin-generic', 'booking-rules' => 'dashicons-calendar-alt', 'statuses' => 'dashicons-yes-alt', 'customer-identity' => 'dashicons-admin-users', 'cancellation-rescheduling' => 'dashicons-update', 'communication' => 'dashicons-email-alt', 'payments' => 'dashicons-money-alt', 'notifications' => 'dashicons-bell', 'privacy-data' => 'dashicons-shield', 'integrations' => 'dashicons-admin-links', 'advanced' => 'dashicons-admin-tools', 'pro' => 'dashicons-star-filled', 'account-license' => 'dashicons-admin-network' )[ $tab ] ?? 'dashicons-admin-generic'; } /** Standalone provider forms stay outside the shared settings save form. */ private function render_grouped_settings_tools( string $current_tab ): void { $modules = $this->modules(); foreach ( array( 'payments' => array('payments','online-payments'), 'communication' => array('reminders','sms'), 'notifications' => array('notification-settings'), 'integrations' => array('notifications','calendars'), 'advanced' => array('pro-setup') ) as $tab => $slugs ) { echo '
'; foreach ( $slugs as $slug ) { if ( ! isset( $modules[ $slug ] ) || ! current_user_can( $modules[ $slug ]['capability'] ) ) { continue; } echo '

' . esc_html( $slug === 'payments' ? __( 'Pay on site','abientot' ) : $modules[ $slug ]['title'] ) . '

'; if ( $slug === 'payments' ) { $this->payment_methods_manager->render_payment_methods(); } else { apply_filters( 'abientot_render_admin_module', false, $slug ); } echo '
'; } echo '
'; } } /** * @return array */ private function settings_tabs(): array { $tabs = array( 'general' => __( 'General', 'abientot' ), 'communication' => __( 'Communication', 'abientot' ), 'payments' => __( 'Payments', 'abientot' ), 'notifications' => __( 'Notifications', 'abientot' ), 'integrations' => __( 'Integrations', 'abientot' ), 'privacy-data' => __( 'Privacy & Data', 'abientot' ), 'advanced' => __( 'Advanced', 'abientot' ), 'pro' => __( 'Abientot Pro', 'abientot' ), ); $modules = $this->modules(); if ( ! isset( $modules['notifications'] ) && ! isset( $modules['calendars'] ) ) { unset( $tabs['integrations'] ); } /** * Filters settings tabs. * * @param array $tabs Settings tabs. */ return apply_filters( 'abientot_settings_tabs', $tabs ); } private function open_page( string $title ): void { $settings = Settings::values(); $business_name = trim( (string) $settings['business_name'] ); if ( '' === $business_name ) { $business_name = __( 'Abientot', 'abientot' ); } $name_classes = array( 'abientot-admin-brand-business' ); foreach ( Settings::header_name_choices() as $part => $choices ) { $value = (string) ( $settings[ 'header_name_' . $part ] ?? '' ); $name_classes[] = 'is-' . $part . '-' . ( isset( $choices[ $value ] ) ? $value : (string) Settings::defaults()[ 'header_name_' . $part ] ); } $name_color = sanitize_hex_color( (string) ( $settings['header_name_color'] ?? '' ) ) ?: '#131f29'; ?>
>

'; } private function render_navigation( string $active ): void { $modules = $this->modules(); echo ''; } /** Native premium pages reuse the shell without preloading their form state. */ public function render_external_module( string $slug, callable $content, ?string $title = null ): void { $employee = ! current_user_can( 'abientot_manage_bookings' ) && current_user_can( 'abientot_view_own_schedule' ); $employee_view = (string) ( \Abientot\Support\Request::view( 'agenda_schedule', 'key' ) ?? '' ); $employee_links = $employee ? $this->employee_links() : array(); $this->open_page( $employee ? ( $employee_links[ $employee_view ][1] ?? __( 'My day','abientot' ) ) : ( $title ?? __( 'Daily agenda','abientot' ) ) ); echo '
'; if ( $employee ) { echo ''; } else { $this->render_navigation( $slug ); } echo '
'; $content(); echo '
'; $this->close_page(); } /** * Staff-account navigation: view key => array( url, label ). Pro may add views (e.g. shared reports). * * @return array */ private function employee_links(): array { $base = admin_url( 'admin.php?page=abientot-my-schedule' ); $links = array( '' => array( $base, __( 'My day', 'abientot' ) ), 'weekly' => array( add_query_arg( 'agenda_schedule', 'weekly', $base ), __( 'My calendar', 'abientot' ) ), 'profile' => array( add_query_arg( 'agenda_schedule', 'profile', $base ), __( 'Profile', 'abientot' ) ), ); return (array) apply_filters( 'abientot_employee_navigation', $links ); } private function module_icon( string $slug ): string { $icons = array( 'today' => 'dashicons-calendar-alt', 'dashboard' => 'dashicons-chart-bar', 'bookings' => 'dashicons-tickets-alt', 'calendar' => 'dashicons-calendar-alt', 'customers' => 'dashicons-groups', 'services' => 'dashicons-list-view', 'staff' => 'dashicons-businessperson', 'locations' => 'dashicons-location', 'form' => 'dashicons-feedback', 'payments' => 'dashicons-money-alt', 'loyalty' => 'dashicons-awards', 'settings' => 'dashicons-admin-generic', ); return $icons[ $slug ] ?? 'dashicons-admin-page'; } /** * @param array $settings Settings. */ private function text_field( string $key, string $label, array $settings, string $description = '' ): void { $this->field_row( $key, $label, '', $description ); } /** * @param array $settings Settings. */ private function email_field( string $key, string $label, array $settings ): void { $this->field_row( $key, $label, '', '' ); } /** * @param array $settings Settings. */ private function number_field( string $key, string $label, array $settings, int $min, int $max, string $description ): void { $html = ''; $this->field_row( $key, $label, $html, $description ); } /** * @param array $settings Settings. * @param array $choices Choices. */ private function select_field( string $key, string $label, array $settings, array $choices, string $description = '' ): void { $html = ''; $this->field_row( $key, $label, $html, $description ); } /** * @param array $settings Settings. */ private function checkbox_field( string $key, string $label, array $settings, string $description = '' ): void { $html = ''; $this->field_row( $key, $label, $html, $description ); } /** * @param array $settings Settings. */ private function color_field( string $key, string $label, array $settings ): void { $html = ''; $this->field_row( $key, $label, $html, '' ); } /** * @param array $settings Settings. */ private function textarea_field( string $key, string $label, array $settings ): void { $html = ''; $this->field_row( $key, $label, $html, '' ); } /** * @param array $settings Settings. */ private function timezone_field( string $key, string $label, array $settings ): void { $html = ''; $this->field_row( $key, $label, $html, '' ); } private function date_format_field( array $settings ): void { $labels = array( 'wordpress' => __( 'Use WordPress date setting', 'abientot' ), 'ymd-dashes' => __( 'YYYY-MM-DD', 'abientot' ), 'mdy-dashes' => __( 'MM-DD-YYYY', 'abientot' ), 'dmy-dashes' => __( 'DD-MM-YYYY', 'abientot' ), 'dmy-short-year' => __( 'DD-MM-YY', 'abientot' ), 'mdy-slashes' => __( 'MM/DD/YYYY', 'abientot' ), 'dmy-slashes' => __( 'DD/MM/YYYY', 'abientot' ), 'month-day-year' => __( 'Month D, YYYY', 'abientot' ), 'day-month-year' => __( 'D Month YYYY', 'abientot' ), ); $html = ''; $this->field_row( 'date_display_format', __( 'Date display format', 'abientot' ), $html, __( 'Applies to Abientot admin screens, public booking messages, contact fallback pages, and emails.', 'abientot' ) ); } private function time_format_field( array $settings ): void { $labels = array( 'wordpress' => __( 'Use WordPress time setting', 'abientot' ), 'time-24' => __( '24-hour', 'abientot' ), 'time-12' => __( '12-hour with am/pm', 'abientot' ), ); $html = ''; $this->field_row( 'time_display_format', __( 'Time display format', 'abientot' ), $html, __( 'Controls displayed appointment times and available slot labels.', 'abientot' ) ); } private function currency_field( string $key, string $label, array $settings ): void { $current = strtoupper( (string) $settings[ $key ] ); $choices = $this->currency_choices(); $html = ''; $this->field_row( $key, $label, $html, __( 'Service prices use this currency.', 'abientot' ) . ' ' . __( 'Enter an exact price supported by the selected currency. Use whole amounts for zero-decimal currencies; other prices support increments of 0.01.','abientot' ) ); } /** Tags and attributes the settings field helpers produce. */ private static function control_html(): array { return array( 'input' => array( 'type' => true, 'class' => true, 'id' => true, 'name' => true, 'value' => true, 'min' => true, 'max' => true, 'checked' => true ), 'select' => array( 'id' => true, 'name' => true ), 'option' => array( 'value' => true, 'selected' => true ), 'textarea' => array( 'class' => true, 'id' => true, 'name' => true, 'rows' => true ), 'label' => array(), ); } private function field_row( string $key, string $label, string $html, string $description ): void { echo ''; echo ''; echo '' . wp_kses( $html, self::control_html() ); if ( '' !== $description ) { echo '

' . esc_html( $description ) . '

'; } echo ''; } /** * @param array $settings Settings. */ private function render_email_template_fields( array $settings ): void { foreach ( EmailService::template_labels() as $index => $label ) { $template_key = (string) $index; $enabled_key = 'email_' . $template_key . '_enabled'; $subject_key = 'email_' . $template_key . '_subject'; $body_key = 'email_' . $template_key . '_body'; $editor_id = 'abientot_email_' . $template_key . '_body'; $preview = $this->email_service->preview( $template_key, 0 ); $default_subject = EmailService::template_subject_defaults()[ $template_key ]; $default_body = EmailService::template_body_defaults()[ $template_key ]; $language_label = Settings::template_language_label(); echo ''; echo '
'; echo '' . esc_html( $label ) . '' . esc_html( (string) $settings[ $subject_key ] ) . '' . ( ! empty( $settings[ $enabled_key ] ) ? esc_html__( 'Enabled', 'abientot' ) : esc_html__( 'Disabled', 'abientot' ) ) . ''; echo '
'; echo ''; echo '


'; echo '

'; echo '
'; echo '
'; echo ''; wp_editor( (string) $settings[ $body_key ], $editor_id, array( 'textarea_name' => Settings::OPTION_NAME . '[' . $body_key . ']', 'textarea_rows' => 12, 'media_buttons' => true, 'teeny' => false, ) ); echo '
'; echo ' '; echo '
'; echo ''; echo '
'; $this->template_variable_box( $editor_id ); echo '
'; echo '
'; $this->email_preview_modal( $template_key, $label, $preview ); $this->defer_email_test_modal( $template_key, $label ); echo ''; } } private function status_label_fields(): void { echo '' . esc_html__( 'Status names', 'abientot' ) . ''; echo '
'; foreach ( $this->statuses->ordered() as $status ) { printf( '', esc_html( (string) $status['slug'] ), esc_attr( Settings::OPTION_NAME . '[status_labels][' . (int) $status['id'] . ']' ), esc_attr( (string) $status['label'] ) ); } echo '

' . esc_html__( 'The plugin keeps the fixed status meanings internally; these names control how each status is displayed.', 'abientot' ) . '

'; echo ''; } private function template_variable_box( string $editor_id ): void { echo ''; } private function email_preview_modal( string $template_key, string $label, array $preview ): void { echo ''; } /** Test-send forms are printed after the settings form closes (forms cannot nest). */ private function defer_email_test_modal( string $template_key, string $label ): void { $this->deferred_email_test_modals[] = array( $template_key, $label ); } private function email_test_modal( string $template_key, string $label ): void { echo ''; } /** * @return string[] */ private function template_variables(): array { return array( 'business_name', 'booking_id', 'customer_name', 'customer_email', 'customer_phone', 'service_name', 'staff_name', 'location_name', 'status', 'start', 'end', 'price', 'currency', 'payment_status', 'customer_note', 'site_name', 'site_url', 'cancellation_contact', 'rescheduling_contact' ); } /** * @return array */ private function template_variable_groups(): array { return array( __( 'Booking', 'abientot' ) => array( 'booking_id', 'service_name', 'staff_name', 'location_name', 'status', 'start', 'end' ), __( 'Customer', 'abientot' ) => array( 'customer_name', 'customer_email', 'customer_phone', 'customer_note' ), __( 'Payment', 'abientot' ) => array( 'price', 'currency', 'payment_status' ), __( 'Business', 'abientot' ) => array( 'business_name', 'site_name', 'site_url', 'cancellation_contact', 'rescheduling_contact' ), ); } /** * @return array */ private function currency_choices(): array { $names = array( 'AED' => 'UAE Dirham', 'AFN' => 'Afghani', 'ALL' => 'Lek', 'AMD' => 'Armenian Dram', 'ANG' => 'Netherlands Antillean Guilder', 'AOA' => 'Kwanza', 'ARS' => 'Argentine Peso', 'AUD' => 'Australian Dollar', 'AWG' => 'Aruban Florin', 'AZN' => 'Azerbaijan Manat', 'BAM' => 'Convertible Mark', 'BBD' => 'Barbados Dollar', 'BDT' => 'Taka', 'BGN' => 'Bulgarian Lev', 'BHD' => 'Bahraini Dinar', 'BIF' => 'Burundi Franc', 'BMD' => 'Bermudian Dollar', 'BND' => 'Brunei Dollar', 'BOB' => 'Boliviano', 'BOV' => 'Mvdol', 'BRL' => 'Brazilian Real', 'BSD' => 'Bahamian Dollar', 'BTN' => 'Ngultrum', 'BWP' => 'Pula', 'BYN' => 'Belarusian Ruble', 'BZD' => 'Belize Dollar', 'CAD' => 'Canadian Dollar', 'CDF' => 'Congolese Franc', 'CHE' => 'WIR Euro', 'CHF' => 'Swiss Franc', 'CHW' => 'WIR Franc', 'CLF' => 'Unidad de Fomento', 'CLP' => 'Chilean Peso', 'CNY' => 'Yuan Renminbi', 'COP' => 'Colombian Peso', 'COU' => 'Unidad de Valor Real', 'CRC' => 'Costa Rican Colon', 'CUC' => 'Peso Convertible', 'CUP' => 'Cuban Peso', 'CVE' => 'Cabo Verde Escudo', 'CZK' => 'Czech Koruna', 'DJF' => 'Djibouti Franc', 'DKK' => 'Danish Krone', 'DOP' => 'Dominican Peso', 'DZD' => 'Algerian Dinar', 'EGP' => 'Egyptian Pound', 'ERN' => 'Nakfa', 'ETB' => 'Ethiopian Birr', 'EUR' => 'Euro', 'FJD' => 'Fiji Dollar', 'FKP' => 'Falkland Islands Pound', 'GBP' => 'Pound Sterling', 'GEL' => 'Lari', 'GHS' => 'Ghana Cedi', 'GIP' => 'Gibraltar Pound', 'GMD' => 'Dalasi', 'GNF' => 'Guinean Franc', 'GTQ' => 'Quetzal', 'GYD' => 'Guyana Dollar', 'HKD' => 'Hong Kong Dollar', 'HNL' => 'Lempira', 'HRK' => 'Kuna', 'HTG' => 'Gourde', 'HUF' => 'Forint', 'IDR' => 'Rupiah', 'ILS' => 'New Israeli Sheqel', 'INR' => 'Indian Rupee', 'IQD' => 'Iraqi Dinar', 'IRR' => 'Iranian Rial', 'ISK' => 'Iceland Krona', 'JMD' => 'Jamaican Dollar', 'JOD' => 'Jordanian Dinar', 'JPY' => 'Yen', 'KES' => 'Kenyan Shilling', 'KGS' => 'Som', 'KHR' => 'Riel', 'KMF' => 'Comorian Franc', 'KPW' => 'North Korean Won', 'KRW' => 'Won', 'KWD' => 'Kuwaiti Dinar', 'KYD' => 'Cayman Islands Dollar', 'KZT' => 'Tenge', 'LAK' => 'Lao Kip', 'LBP' => 'Lebanese Pound', 'LKR' => 'Sri Lanka Rupee', 'LRD' => 'Liberian Dollar', 'LSL' => 'Loti', 'LYD' => 'Libyan Dinar', 'MAD' => 'Moroccan Dirham', 'MDL' => 'Moldovan Leu', 'MGA' => 'Malagasy Ariary', 'MKD' => 'Denar', 'MMK' => 'Kyat', 'MNT' => 'Tugrik', 'MOP' => 'Pataca', 'MRU' => 'Ouguiya', 'MUR' => 'Mauritius Rupee', 'MVR' => 'Rufiyaa', 'MWK' => 'Malawi Kwacha', 'MXN' => 'Mexican Peso', 'MXV' => 'Mexican Unidad de Inversion (UDI)', 'MYR' => 'Malaysian Ringgit', 'MZN' => 'Mozambique Metical', 'NAD' => 'Namibia Dollar', 'NGN' => 'Naira', 'NIO' => 'Cordoba Oro', 'NOK' => 'Norwegian Krone', 'NPR' => 'Nepalese Rupee', 'NZD' => 'New Zealand Dollar', 'OMR' => 'Rial Omani', 'PAB' => 'Balboa', 'PEN' => 'Sol', 'PGK' => 'Kina', 'PHP' => 'Philippine Peso', 'PKR' => 'Pakistan Rupee', 'PLN' => 'Zloty', 'PYG' => 'Guarani', 'QAR' => 'Qatari Rial', 'RON' => 'Romanian Leu', 'RSD' => 'Serbian Dinar', 'RUB' => 'Russian Ruble', 'RWF' => 'Rwanda Franc', 'SAR' => 'Saudi Riyal', 'SBD' => 'Solomon Islands Dollar', 'SCR' => 'Seychelles Rupee', 'SDG' => 'Sudanese Pound', 'SEK' => 'Swedish Krona', 'SGD' => 'Singapore Dollar', 'SHP' => 'Saint Helena Pound', 'SLE' => 'Leone', 'SLL' => 'Leone', 'SOS' => 'Somali Shilling', 'SRD' => 'Surinam Dollar', 'SSP' => 'South Sudanese Pound', 'STN' => 'Dobra', 'SVC' => 'El Salvador Colon', 'SYP' => 'Syrian Pound', 'SZL' => 'Lilangeni', 'THB' => 'Baht', 'TJS' => 'Somoni', 'TMT' => 'Turkmenistan New Manat', 'TND' => 'Tunisian Dinar', 'TOP' => 'Pa’anga', 'TRY' => 'Turkish Lira', 'TTD' => 'Trinidad and Tobago Dollar', 'TWD' => 'New Taiwan Dollar', 'TZS' => 'Tanzanian Shilling', 'UAH' => 'Hryvnia', 'UGX' => 'Uganda Shilling', 'USD' => 'US Dollar', 'USN' => 'US Dollar (Next day)', 'UYI' => 'Uruguay Peso en Unidades Indexadas (UI)', 'UYU' => 'Peso Uruguayo', 'UYW' => 'Unidad Previsional', 'UZS' => 'Uzbekistan Sum', 'VED' => 'Bolívar Soberano', 'VES' => 'Bolívar Soberano', 'VND' => 'Dong', 'VUV' => 'Vatu', 'WST' => 'Tala', 'XAF' => 'CFA Franc BEAC', 'XAG' => 'Silver', 'XAU' => 'Gold', 'XBA' => 'Bond Markets Unit European Composite Unit (EURCO)', 'XBB' => 'Bond Markets Unit European Monetary Unit (E.M.U.-6)', 'XBC' => 'Bond Markets Unit European Unit of Account 9 (E.U.A.-9)', 'XBD' => 'Bond Markets Unit European Unit of Account 17 (E.U.A.-17)', 'XCD' => 'East Caribbean Dollar', 'XDR' => 'SDR (Special Drawing Right)', 'XOF' => 'CFA Franc BCEAO', 'XPD' => 'Palladium', 'XPF' => 'CFP Franc', 'XPT' => 'Platinum', 'XSU' => 'Sucre', 'XUA' => 'ADB Unit of Account', 'YER' => 'Yemeni Rial', 'ZAR' => 'Rand', 'ZMW' => 'Zambian Kwacha', 'ZWL' => 'Zimbabwe Dollar', ); $choices = array(); foreach ( Settings::currency_codes() as $code ) { $choices[ $code ] = $code . ' - ' . ( $names[ $code ] ?? $code ); } return $choices; } /** * @return array */ private function timezone_options(): array { $options = array(); foreach ( timezone_identifiers_list() as $timezone ) { if ( false === strpos( $timezone, '/' ) ) { continue; } $options[ $timezone ] = $this->timezone_label( $timezone ); } asort( $options, SORT_NATURAL | SORT_FLAG_CASE ); return $options; } private function timezone_label( string $timezone ): string { try { $zone = new DateTimeZone( $timezone ); $now = new DateTimeImmutable( 'now', $zone ); $location = timezone_location_get( $zone ); $country = ''; if ( is_array( $location ) && ! empty( $location['country_code'] ) && class_exists( '\Locale' ) ) { $country = (string) \Locale::getDisplayRegion( '-' . $location['country_code'], get_user_locale() ); } if ( '' === $country ) { $country = str_replace( '_', ' ', strtok( $timezone, '/' ) ?: $timezone ); } $city = str_replace( '_', ' ', substr( strrchr( $timezone, '/' ) ?: $timezone, 1 ) ); $offset = $zone->getOffset( $now ); $sign = $offset >= 0 ? '+' : '-'; $offset = abs( $offset ); return sprintf( '%s (%s) - GMT%s%02d:%02d', $country, $city, $sign, floor( $offset / HOUR_IN_SECONDS ), floor( ( $offset % HOUR_IN_SECONDS ) / MINUTE_IN_SECONDS ) ); } catch ( Exception $exception ) { return $timezone; } } }