'; public static function register(): void { add_action( 'admin_post_' . self::ACTION, array( self::class, 'handle' ) ); } /** @return array */ public static function state(): array { $state = get_option( self::OPTION, array() ); $state = is_array( $state ) ? $state : array(); return array_merge( array( 'preset' => '', 'done' => array(), 'service_id' => 0, 'page_id' => 0, 'email_confirmed' => '', 'started_at' => '', 'completed_at' => '', 'dismissed_at' => '', ), $state ); } /** @param array $changes */ private static function save_state( array $changes ): void { $state = array_merge( self::state(), $changes ); if ( '' === (string) $state['started_at'] ) { $state['started_at'] = gmdate( 'Y-m-d H:i:s' ); } update_option( self::OPTION, $state, false ); } /** * Shown in the navigation and on Today until it is finished or hidden. Sites that already * have an active service on a published booking page never see it unless they open it. */ public static function visible(): bool { static $visible = null; if ( ! current_user_can( self::CAPABILITY ) ) { return false; } if ( null === $visible ) { $state = self::state(); $visible = '' === (string) $state['completed_at'] && '' === (string) $state['dismissed_at'] && ! ( '' === (string) $state['started_at'] && self::has_active_service() && ( new BookingPagesRepository() )->published( 1 ) ); } return $visible; } public static function url( string $step = '' ): string { $args = array( 'page' => 'abientot-setup' ); if ( '' !== $step ) { $args['step'] = $step; } return add_query_arg( $args, admin_url( 'admin.php' ) ); } /** Short prompt for the Today screen. */ public static function render_prompt(): void { if ( ! self::visible() ) { return; } $started = '' !== (string) self::state()['started_at']; echo '

' . esc_html( $started ? __( 'Continue your guided setup', 'abientot' ) : __( 'Get your booking page ready', 'abientot' ) ) . '

' . esc_html__( 'Five short steps: your business, a first service, your hours, a published booking page and a test booking.', 'abientot' ) . '

'; echo '

' . esc_html( $started ? __( 'Continue setup', 'abientot' ) : __( 'Start guided setup', 'abientot' ) ) . '

'; } /** * Business types. Each one only prefills the service and hours steps. * * @return array>}> */ public static function presets(): array { $weekdays = static function ( array $days, string $start, string $end ): array { $out = array(); foreach ( $days as $day ) { $out[ $day ] = array( array( $start, $end ) ); } return $out; }; $tutor = $weekdays( array( 1, 2, 3, 4, 5 ), '15:00', '20:00' ); $tutor[6] = array( array( '10:00', '14:00' ) ); return array( 'salon' => array( 'label' => __( 'Hair or beauty salon', 'abientot' ), 'service' => __( 'Haircut', 'abientot' ), 'duration' => 45, 'buffer' => 15, 'hours' => $weekdays( array( 2, 3, 4, 5, 6 ), '09:00', '18:00' ), ), 'tutor' => array( 'label' => __( 'Tutor, teacher or coach', 'abientot' ), 'service' => __( 'Private lesson', 'abientot' ), 'duration' => 60, 'buffer' => 10, 'hours' => $tutor, ), 'consultant' => array( 'label' => __( 'Consultant or advisor', 'abientot' ), 'service' => __( 'Consultation', 'abientot' ), 'duration' => 60, 'buffer' => 15, 'hours' => $weekdays( array( 1, 2, 3, 4, 5 ), '09:00', '17:00' ), ), 'other' => array( 'label' => __( 'Something else', 'abientot' ), 'service' => __( 'Appointment', 'abientot' ), 'duration' => 30, 'buffer' => 0, 'hours' => $weekdays( array( 1, 2, 3, 4, 5 ), '09:00', '17:00' ), ), ); } /** @return array */ private static function step_labels(): array { return array( 'business' => __( 'Your business', 'abientot' ), 'service' => __( 'First service', 'abientot' ), 'hours' => __( 'Opening hours', 'abientot' ), 'page' => __( 'Booking page', 'abientot' ), 'test' => __( 'Test booking', 'abientot' ), ); } private static function current_step(): string { $step = (string) ( Request::view( 'step', 'key' ) ?? '' ); if ( 'done' === $step || in_array( $step, self::STEPS, true ) ) { return $step; } $done = (array) self::state()['done']; foreach ( self::STEPS as $candidate ) { if ( empty( $done[ $candidate ] ) ) { return $candidate; } } return 'done'; } /* -------------------------------------------------------------- Render */ public static function render(): void { if ( ! current_user_can( self::CAPABILITY ) ) { echo '

' . esc_html__( 'You do not have permission to manage settings.', 'abientot' ) . '

'; return; } $step = self::current_step(); $state = self::state(); $done = (array) $state['done']; echo '
'; echo '

' . esc_html__( 'Guided setup', 'abientot' ) . '

' . esc_html__( 'Get a booking page your customers can use. You can change everything later, and you can leave and come back at any time.', 'abientot' ) . '

'; self::notice(); echo '
    '; foreach ( self::step_labels() as $key => $label ) { $classes = array(); if ( ! empty( $done[ $key ] ) ) { $classes[] = 'is-complete'; } if ( $key === $step ) { $classes[] = 'is-current'; } echo '
  1. ' . esc_html( $label ) . '' . ( ! empty( $done[ $key ] ) ? ' ' . esc_html__( 'Complete', 'abientot' ) . '' : '' ) . '
  2. '; } echo '
'; echo '
'; switch ( $step ) { case 'business': self::render_business( $state ); break; case 'service': self::render_service( $state ); break; case 'hours': self::render_hours( $state ); break; case 'page': self::render_page( $state ); break; case 'test': self::render_test( $state ); break; default: self::render_done(); } echo '
'; if ( 'done' !== $step ) { echo '
'; self::hidden( 'dismiss' ); echo ' ' . esc_html__( 'Your settings stay as they are. Open it again from Reports → Finish setting up.', 'abientot' ) . '
'; } echo '
'; } private static function hidden( string $step ): void { wp_nonce_field( self::ACTION ); echo ''; } private static function open_form( string $step ): void { echo '
'; self::hidden( $step ); } private static function actions( string $primary, string $skip_step = '' ): void { echo '

'; if ( '' !== $skip_step ) { echo ' '; } echo '

'; } /** @param array $state */ private static function render_business( array $state ): void { $settings = Settings::values(); $staff = ( new StaffRepository() )->default_staff(); $name = (string) ( $staff['display_name'] ?? '' ); if ( '' === $name || __( 'Default Staff', 'abientot' ) === $name || 'Default Staff' === $name ) { $name = (string) wp_get_current_user()->display_name; } $preset = (string) $state['preset']; echo '

' . esc_html__( 'Tell us about your business', 'abientot' ) . '

'; self::open_form( 'business' ); echo '
' . esc_html__( 'What kind of business is it?', 'abientot' ) . '

' . esc_html__( 'This only suggests a first service and opening hours. You can change them in the next steps.', 'abientot' ) . '

'; foreach ( self::presets() as $key => $choice ) { echo ''; } echo '
'; echo '

'; echo '

' . esc_html__( 'Your name, or the name of the person who takes the appointments.', 'abientot' ) . '

'; echo '

' . esc_html__( 'Booking times are shown in this timezone.', 'abientot' ) . '

'; echo '

'; self::actions( __( 'Save and continue', 'abientot' ) ); } /** @param array $state */ private static function render_service( array $state ): void { if ( ! current_user_can( 'abientot_manage_services' ) ) { self::no_permission( 'service' ); return; } $services = array_values( array_filter( ( new ServicesRepository() )->list_for_admin(), static fn( array $service ): bool => 'active' === (string) ( $service['status'] ?? '' ) ) ); $presets = self::presets(); $preset = $presets[ (string) $state['preset'] ] ?? $presets['other']; $own = (int) $state['service_id'] > 0 ? ( new ServicesRepository() )->find( (int) $state['service_id'] ) : null; $currency = (string) Settings::values()['currency']; echo '

' . esc_html__( 'Add the first service customers can book', 'abientot' ) . '

'; if ( $services && ! $own ) { echo '

' . esc_html__( 'You already have these active services:', 'abientot' ) . '

    '; foreach ( array_slice( $services, 0, 10 ) as $service ) { /* translators: 1: service name, 2: duration in minutes. */ echo '
  • ' . esc_html( sprintf( __( '%1$s (%2$d min)', 'abientot' ), (string) $service['name'], (int) $service['duration_minutes'] ) ) . '
  • '; } echo '
'; self::open_form( 'service' ); echo ''; echo '

' . esc_html__( 'Manage services', 'abientot' ) . '

'; return; } $name = $own ? (string) $own['name'] : $preset['service']; $duration = $own ? (int) $own['duration_minutes'] : $preset['duration']; $buffer = $own ? (int) $own['buffer_after_minutes'] : $preset['buffer']; $price = $own && (int) $own['price_cents'] > 0 ? MoneyInput::format( (int) $own['price_cents'] ) : ''; self::open_form( 'service' ); echo '

'; echo '

'; echo '

' . esc_html__( 'Time to tidy up or prepare before the next customer.', 'abientot' ) . '

'; /* translators: %s: currency code, for example EUR. */ echo '

' . esc_html__( 'Leave empty to show no price. Customers pay on site unless you set up online payments.', 'abientot' ) . '

'; self::actions( $own ? __( 'Save and continue', 'abientot' ) : __( 'Add service and continue', 'abientot' ), 'service' ); } /** @param array $state */ private static function render_hours( array $state ): void { if ( ! current_user_can( 'abientot_manage_staff' ) ) { self::no_permission( 'hours' ); return; } echo '

' . esc_html__( 'When can customers book?', 'abientot' ) . '

'; if ( count( ( new StaffRepository() )->all( 2 ) ) > 1 ) { echo '

' . esc_html__( 'Your team has several staff members, so working hours are set for each person on the Team screen.', 'abientot' ) . '

'; self::open_form( 'hours' ); echo ''; echo '

' . esc_html__( 'Edit working hours', 'abientot' ) . '

'; return; } $staff = ( new StaffRepository() )->default_staff(); $rules = $staff ? ( new AvailabilityRepository() )->weekly_rules_for_staff( (int) $staff['id'] ) : array(); $presets = self::presets(); $suggest = empty( $state['done']['hours'] ) && ( ! $rules || self::is_seeded_schedule( $rules ) ) && isset( $presets[ (string) $state['preset'] ] ); $schedule = $suggest ? self::preset_schedule( $presets[ (string) $state['preset'] ]['hours'] ) : ScheduleEditor::from_rules( $rules ); echo '

' . esc_html( $suggest ? __( 'We suggest these hours for your type of business. Change them to match your week.', 'abientot' ) : __( 'These are your current working hours. Change them if needed.', 'abientot' ) ) . '

'; self::open_form( 'hours' ); ScheduleEditor::render( 'schedule', $schedule ); if ( is_array( get_option( 'abientot_location_weekly_schedule', null ) ) ) { echo '

' . esc_html__( 'Your location opening hours are updated to the same hours.', 'abientot' ) . '

'; } self::actions( __( 'Save hours and continue', 'abientot' ), 'hours' ); } /** @param array $state */ private static function render_page( array $state ): void { $pages = ( new BookingPagesRepository() )->published( 3 ); echo '

' . esc_html__( 'Put the booking form on your website', 'abientot' ) . '

'; if ( $pages ) { echo '

' . esc_html__( 'Your booking form is on this published page:', 'abientot' ) . '

'; self::open_form( 'page' ); echo ''; echo '

'; return; } $draft = (int) $state['page_id'] > 0 ? get_post( (int) $state['page_id'] ) : null; if ( $draft instanceof \WP_Post && 'trash' !== $draft->post_status ) { echo '

' . esc_html__( 'Your booking page was created as a draft. Publish it when it is ready, then come back here.', 'abientot' ) . '

'; $edit = get_edit_post_link( $draft->ID, 'raw' ); if ( $edit ) { echo '

' . esc_html__( 'Edit the booking page', 'abientot' ) . '

'; } } else { echo '

' . esc_html__( 'We create a page with the booking form on it. You can edit the page title and add text or pictures later, like any other page.', 'abientot' ) . '

'; } if ( ! current_user_can( 'edit_pages' ) ) { echo '

' . esc_html__( 'Ask your website administrator to add the Abientot Booking Form block or the shortcode [abientot_booking_form] to a page.', 'abientot' ) . '

'; return; } self::open_form( 'page' ); if ( ! ( $draft instanceof \WP_Post && 'trash' !== $draft->post_status ) ) { echo '

'; } echo '

'; if ( current_user_can( 'publish_pages' ) ) { echo ' '; } if ( ! ( $draft instanceof \WP_Post && 'trash' !== $draft->post_status ) ) { echo ' '; } echo '

'; echo '

' . esc_html__( 'You can also add the Abientot Booking Form block or the shortcode [abientot_booking_form] to any existing page.', 'abientot' ) . '

'; } /** @param array $state */ private static function render_test( array $state ): void { $pages = ( new BookingPagesRepository() )->published( 1 ); $settings = Settings::values(); $recipient = (string) ( $settings['admin_notification_email'] ?: get_option( 'admin_email' ) ); echo '

' . esc_html__( 'Make a test booking, like a customer would', 'abientot' ) . '

'; if ( $pages ) { echo '
  1. ' . esc_html__( 'Open your booking page in a private browser window, so you see it as a visitor who is not logged in.', 'abientot' ) . '
    ' . esc_html__( 'Open the booking page', 'abientot' ) . ' ' . esc_html( $pages[0]['url'] ) . '
  2. '; echo '
  3. ' . esc_html__( 'Book a time with your own email address.', 'abientot' ) . '
  4. '; echo '
  5. ' . esc_html__( 'Check that the confirmation email arrives in your inbox, and look in the spam folder too.', 'abientot' ) . '
'; } else { echo '

' . esc_html__( 'Your booking form is not on a published page yet. Publish it first, then make a test booking from it.', 'abientot' ) . ' ' . esc_html__( 'Go to the booking page step', 'abientot' ) . '

'; } $booking = self::latest_public_booking( (string) $state['started_at'] ); if ( $booking ) { echo '

' . esc_html__( 'We received a booking from your booking page. You can cancel it in Bookings when you are done testing.', 'abientot' ) . ' ' . esc_html__( 'Open Bookings', 'abientot' ) . '

'; } echo '
' . esc_html__( 'Or send yourself a test email first', 'abientot' ) . ''; self::open_form( 'send_test' ); echo '

'; echo '

'; echo '
' . esc_html__( 'Did the email arrive?', 'abientot' ) . ''; self::open_form( 'test' ); echo '

'; if ( 'no' === (string) $state['email_confirmed'] ) { self::render_email_help(); } } private static function render_email_help(): void { echo '

' . esc_html__( 'When emails do not arrive', 'abientot' ) . '

    '; foreach ( array( __( 'Look in the spam or junk folder, and search your inbox for the business name.', 'abientot' ), __( 'Many hosts do not deliver email sent by WordPress itself. An SMTP plugin sends it through a real mailbox or an email service instead, which fixes most delivery problems.', 'abientot' ), __( 'Check the sender address in Settings → Communication: use an address on your own domain.', 'abientot' ), __( 'Send another test email after each change.', 'abientot' ), ) as $tip ) { echo '
  • ' . esc_html( $tip ) . '
  • '; } echo '

' . esc_html__( 'Open email settings', 'abientot' ) . '

'; } private static function render_done(): void { $state = self::state(); echo '

' . esc_html__( 'Your booking page is ready', 'abientot' ) . '

'; echo '

' . esc_html__( 'Customers can now book online. Share the link wherever they look for you.', 'abientot' ) . '

'; if ( 'no' === (string) $state['email_confirmed'] ) { self::render_email_help(); } ShareKit::render(); echo '

' . esc_html__( 'Next, when you need them', 'abientot' ) . '

    '; foreach ( array( 'abientot-services' => __( 'Add more services, descriptions and pictures', 'abientot' ), 'abientot-form' => __( 'Choose the look of your booking form', 'abientot' ), 'abientot-settings' => __( 'Set booking rules: notice time, cancellations and email texts', 'abientot' ), ) as $page => $label ) { echo '
  • ' . esc_html( $label ) . '
  • '; } echo '
'; if ( '' === (string) $state['completed_at'] ) { self::open_form( 'finish' ); echo '

'; } } private static function no_permission( string $step ): void { echo '

' . esc_html__( 'Your account cannot change this part of the setup. Ask your website administrator, or skip this step.', 'abientot' ) . '

'; self::open_form( $step ); echo '

'; } private static function notice(): void { $notice = RedirectNotice::get( 'abientot_notice' ); $map = array( 'setup-saved' => array( 'success', __( 'Saved.', 'abientot' ) ), 'setup-email-sent' => array( 'success', __( 'Test email sent. Check your inbox.', 'abientot' ) ), 'setup-email-failed' => array( 'error', __( 'WordPress could not send the test email. Your site’s email sending needs to be set up.', 'abientot' ) ), 'setup-missing' => array( 'error', __( 'Fill in the required fields.', 'abientot' ) ), 'setup-currency' => array( 'error', __( 'Some saved service or option prices contain fractions. Adjust those prices before choosing a whole-unit currency.', 'abientot' ) ), 'setup-price' => array( 'error', __( 'Enter a valid price for this currency, or leave it empty.', 'abientot' ) ), 'setup-schedule' => array( 'error', __( 'Check working periods: use valid times, different start and end times, and no overlapping periods.', 'abientot' ) ), 'setup-page-failed' => array( 'error', __( 'The page could not be created. Please try again.', 'abientot' ) ), ); if ( isset( $map[ $notice ] ) ) { echo '

' . esc_html( $map[ $notice ][1] ) . '

'; } } /* -------------------------------------------------------------- Save */ public static function handle(): void { $input = Input::post( self::ACTION ); if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to perform this action.', 'abientot' ), '', array( 'response' => 403 ) ); } $step = $input->key( 'setup_step' ); $skip = '1' === $input->text( 'setup_skip' ); switch ( $step ) { case 'dismiss': self::save_state( array( 'dismissed_at' => gmdate( 'Y-m-d H:i:s' ) ) ); self::redirect( admin_url( 'admin.php?page=abientot-today' ) ); break; case 'finish': self::save_state( array( 'completed_at' => gmdate( 'Y-m-d H:i:s' ) ) ); self::redirect( admin_url( 'admin.php?page=abientot-today' ) ); break; case 'send_test': $result = ( new EmailService() )->send_test( 'customer_confirmation', $input->email( 'recipient' ) ); self::save_state( array() ); self::redirect( self::url( 'test' ), is_wp_error( $result ) ? 'setup-email-failed' : 'setup-email-sent' ); break; case 'business': case 'service': case 'hours': case 'page': case 'test': if ( ! $skip ) { $method = 'save_' . $step; self::$method( $input ); } self::complete( $step ); break; default: wp_die( esc_html__( 'Invalid request.', 'abientot' ), '', array( 'response' => 400 ) ); } } private static function complete( string $step ): void { $state = self::state(); $done = (array) $state['done']; $done[ $step ] = 1; self::save_state( array( 'done' => $done ) ); $index = array_search( $step, self::STEPS, true ); $next = self::STEPS[ (int) $index + 1 ] ?? 'done'; self::redirect( self::url( $next ), 'setup-saved' ); } private static function save_business( Input $input ): void { $presets = self::presets(); $preset = $input->key( 'preset' ); $name = $input->text( 'business_name' ); $person = $input->text( 'staff_name' ); if ( ! isset( $presets[ $preset ] ) || '' === $name || '' === $person ) { self::redirect( self::url( 'business' ), 'setup-missing' ); } $settings = new Settings(); $previous = (string) Settings::values()['currency']; try { // Same sanitizing and currency checks as a settings import; takes the configuration lock. $values = $settings->sanitize_import( array( 'business_name' => $name, 'business_timezone' => $input->text( 'business_timezone' ), 'currency' => $input->text( 'currency' ), ) ); } catch ( \InvalidArgumentException $error ) { self::redirect( self::url( 'business' ), 'setup-currency' ); } update_option( Settings::OPTION_NAME, $values, false ); if ( $previous !== (string) $values['currency'] ) { ( new ServicesRepository() )->update_all_currency( (string) $values['currency'] ); } $timezone = (string) $values['business_timezone']; $now = gmdate( 'Y-m-d H:i:s' ); $staff = ( new StaffRepository() )->default_staff(); if ( $staff && current_user_can( 'abientot_manage_staff' ) ) { ( new StaffRepository() )->update( (int) $staff['id'], array( 'display_name' => $person, 'timezone' => $timezone, 'updated_at' => $now, ) ); } $location = ( new LocationsRepository() )->default_location(); if ( $location && current_user_can( 'abientot_manage_locations' ) ) { $data = array( 'timezone' => $timezone, 'updated_at' => $now, ); if ( in_array( (string) $location['name'], array( '', 'Default Location', __( 'Default Location', 'abientot' ) ), true ) ) { $data['name'] = $name; } ( new LocationsRepository() )->update( (int) $location['id'], $data ); } self::save_state( array( 'preset' => $preset ) ); } private static function save_service( Input $input ): void { if ( ! current_user_can( 'abientot_manage_services' ) ) { wp_die( esc_html__( 'You do not have permission to perform this action.', 'abientot' ), '', array( 'response' => 403 ) ); } BookingLock::require_configuration_lock(); $name = $input->text( 'service_name' ); if ( '' === $name ) { self::redirect( self::url( 'service' ), 'setup-missing' ); } $currency = (string) Settings::values()['currency']; try { $price = '' === trim( $input->text( 'price' ) ) ? 0 : MoneyInput::parse( $input->text( 'price' ), $currency ); Currency::validate( $price, $currency ); } catch ( \InvalidArgumentException $error ) { self::redirect( self::url( 'service' ), 'setup-price' ); } $repository = new ServicesRepository(); $existing = (int) self::state()['service_id'] > 0 ? $repository->find( (int) self::state()['service_id'] ) : null; $now = gmdate( 'Y-m-d H:i:s' ); $data = array( 'name' => $name, 'slug' => self::unique_slug( $repository, $name, $existing ? (int) $existing['id'] : 0 ), 'duration_minutes' => max( 5, min( 1440, $input->int( 'duration_minutes', 30 ) ) ), 'buffer_after_minutes' => max( 0, min( 240, $input->int( 'buffer_after_minutes' ) ) ), 'price_cents' => $price, 'currency' => $currency, 'updated_at' => $now, ); if ( $existing ) { $repository->update( (int) $existing['id'], $data ); return; } $id = $repository->insert( array_merge( $data, array( 'category_id' => null, 'description' => '', 'image_id' => null, 'buffer_before_minutes' => 0, 'color' => '#172033', 'status' => 'active', 'visibility' => 'public', 'sort_order' => 0, 'created_at' => $now, ) ) ); $staff = ( new StaffRepository() )->default_staff(); if ( $id > 0 && $staff ) { $repository->link_to_staff( $id, (int) $staff['id'] ); } self::save_state( array( 'service_id' => $id ) ); } private static function save_hours( Input $input ): void { if ( ! current_user_can( 'abientot_manage_staff' ) ) { wp_die( esc_html__( 'You do not have permission to perform this action.', 'abientot' ), '', array( 'response' => 403 ) ); } BookingLock::require_configuration_lock(); if ( count( ( new StaffRepository() )->all( 2 ) ) > 1 ) { return; } $staff = ( new StaffRepository() )->default_staff(); $location = ( new LocationsRepository() )->default_location(); if ( ! $staff ) { return; } try { if ( ! $input->has_list( 'schedule' ) ) { throw new \InvalidArgumentException(); } $map = ScheduleEditor::posted( $input->text_list( 'schedule' ) ); } catch ( \InvalidArgumentException $error ) { self::redirect( self::url( 'hours' ), 'setup-schedule' ); } $rules = array(); foreach ( $map as $weekday => $entry ) { foreach ( $entry['periods'] as $period ) { $rules[] = array( 'location_id' => $location ? (int) $location['id'] : null, 'weekday' => ( $weekday + (int) ( $period['day_offset'] ?? 0 ) ) % 7, 'start_time' => $period['start_time'], 'end_time' => $period['end_time'], 'rule_type' => 'working', ); } } ( new AvailabilityRepository() )->replace_weekly_rules( (int) $staff['id'], $rules ); // One person, one place: saved location hours follow, so they cannot hide the new hours. if ( is_array( get_option( 'abientot_location_weekly_schedule', null ) ) && current_user_can( 'abientot_manage_locations' ) ) { update_option( 'abientot_location_weekly_schedule', $map, false ); } } private static function save_page( Input $input ): void { $status = 'publish' === $input->key( 'page_status' ) ? 'publish' : 'draft'; if ( ! current_user_can( 'publish' === $status ? 'publish_pages' : 'edit_pages' ) ) { wp_die( esc_html__( 'You do not have permission to perform this action.', 'abientot' ), '', array( 'response' => 403 ) ); } $draft = (int) self::state()['page_id'] > 0 ? get_post( (int) self::state()['page_id'] ) : null; if ( $draft instanceof \WP_Post && 'trash' !== $draft->post_status ) { if ( 'publish' === $status && 'publish' !== $draft->post_status && current_user_can( 'publish_post', $draft->ID ) ) { wp_publish_post( $draft->ID ); } if ( 'publish' !== $status ) { self::redirect( self::url( 'page' ) ); } return; } $title = $input->text( 'page_title' ); $id = wp_insert_post( array( 'post_type' => 'page', 'post_status' => $status, 'post_title' => '' !== $title ? $title : __( 'Book an appointment', 'abientot' ), 'post_content' => self::BLOCK, ), true ); if ( is_wp_error( $id ) || ! $id ) { self::redirect( self::url( 'page' ), 'setup-page-failed' ); } self::save_state( array( 'page_id' => (int) $id ) ); if ( 'publish' !== $status ) { // A draft is not a booking page yet: stay on this step until it is published. self::redirect( self::url( 'page' ), 'setup-saved' ); } } private static function save_test( Input $input ): void { $answer = $input->key( 'email_arrived' ); self::save_state( array( 'email_confirmed' => in_array( $answer, array( 'yes', 'no' ), true ) ? $answer : '' ) ); } /* -------------------------------------------------------------- Helpers */ private static function redirect( string $url, string $notice = '' ): void { wp_safe_redirect( '' !== $notice ? RedirectNotice::url( $url, array( 'abientot_notice' => $notice ) ) : $url ); exit; } private static function has_active_service(): bool { foreach ( ( new ServicesRepository() )->list_for_admin() as $service ) { if ( 'active' === (string) ( $service['status'] ?? '' ) ) { return true; } } return false; } /** True when the hours are still the Monday–Friday 9:00–17:00 schedule created at install. */ private static function is_seeded_schedule( array $rules ): bool { $days = array(); foreach ( $rules as $rule ) { if ( 'working' !== (string) $rule['rule_type'] || '09:00:00' !== (string) $rule['start_time'] || '17:00:00' !== (string) $rule['end_time'] || ! empty( $rule['service_id'] ) ) { return false; } $days[] = (int) $rule['weekday']; } sort( $days ); return array( 1, 2, 3, 4, 5 ) === $days; } /** * @param array> $hours Preset hours by weekday (0 = Sunday). * @return array}> */ private static function preset_schedule( array $hours ): array { $out = array(); foreach ( range( 0, 6 ) as $day ) { $periods = array(); foreach ( $hours[ $day ] ?? array() as $period ) { $periods[] = array( 'start_time' => $period[0] . ':00', 'end_time' => $period[1] . ':00', ); } $out[ $day ] = array( 'periods' => $periods ); } return $out; } private static function unique_slug( ServicesRepository $repository, string $name, int $current_id ): string { $base = sanitize_title( $name ) ?: 'service'; $slug = $base; for ( $i = 2; $i < 1000; $i++ ) { $existing = $repository->find_by_slug( $slug ); if ( ! $existing || ( $current_id > 0 && (int) $existing['id'] === $current_id ) ) { return $slug; } $slug = $base . '-' . $i; } return $base . '-' . wp_generate_password( 6, false, false ); } /** @return array|null */ private static function latest_public_booking( string $since ): ?array { if ( '' === $since ) { return null; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One indexed lookup on the setup screen only. $row = $wpdb->get_row( $wpdb->prepare( "SELECT id, start_utc FROM {$wpdb->prefix}abientot_appointments WHERE source = %s AND created_at >= %s ORDER BY id DESC LIMIT 1", 'frontend', $since ), ARRAY_A ); return is_array( $row ) ? $row : null; } }