scheduler = $scheduler; $this->status_manager = $status_manager; } /** * Register admin hooks. * * @return void */ public function register() { add_action( 'admin_post_abct_save_schedule_settings', array( $this, 'handle_save' ) ); add_action( 'wp_ajax_abct_toggle_manual_mode', array( $this, 'ajax_toggle_manual_mode' ) ); } /** * Render schedule page. * * @return void */ public function render_page() { if ( ! $this->can_manage_schedule() ) { return; } $settings = $this->scheduler->get_settings(); $shift_2_min_time = $this->get_next_minute_time( isset( $settings['lunch_close'] ) ? (string) $settings['lunch_close'] : '' ); $manual_mode = $this->status_manager->get_manual_mode(); $state = $this->status_manager->get_state(); $message = sanitize_textarea_field( (string) get_option( ABCT_Store_Status_Manager::CLOSED_MESSAGE_OPTION, '' ) ); $poll_interval_ms = max( 5000, absint( apply_filters( 'abct_store_status_poll_interval_ms', 10000 ) ) ); ?>

get_schedule_validation_error_message( sanitize_key( wp_unslash( $_GET['abct_schedule_error'] ) ) ) ); ?>

can_manage_schedule() ) { wp_die( esc_html__( 'You are not allowed to do this.', 'abct-smart-orders' ) ); } check_admin_referer( 'abct_save_schedule_settings', 'abct_schedule_nonce' ); if ( 'offline' === $this->status_manager->get_manual_mode() ) { wp_safe_redirect( add_query_arg( array( 'page' => 'abct-edit-schedule', 'abct_schedule_error' => 'manual_offline_blocked', ), admin_url( 'admin.php' ) ) ); exit; } $raw_schedule = isset( $_POST['abct_opening_hours'] ) ? wp_unslash( $_POST['abct_opening_hours'] ) : array(); $validation_error = $this->validate_schedule_configuration( $raw_schedule ); if ( '' !== $validation_error ) { wp_safe_redirect( add_query_arg( array( 'page' => 'abct-edit-schedule', 'abct_schedule_error' => $validation_error, ), admin_url( 'admin.php' ) ) ); exit; } $schedule = $this->scheduler->sanitize_settings( $raw_schedule ); update_option( ABCT_Scheduler::OPTION_KEY, $schedule ); // When 24-hour mode is disabled, immediately return control to the real schedule. // Any previous manual/24h override must be removed so the store state is calculated // only from the selected weekdays plus Shift 1 and Shift 2. if ( empty( $schedule['open_24_hours'] ) ) { delete_option( ABCT_Store_Status_Manager::MANUAL_OVERRIDE_OPTION ); update_option( ABCT_Store_Status_Manager::MANUAL_OVERRIDE_OPTION, array() ); update_option( ABCT_Store_Status_Manager::MANUAL_MODE_OPTION, 'online' ); update_option( 'abct_manual_store_status', 'open' ); delete_transient( 'abct_store_state_cache' ); } $closed_message = isset( $_POST['abct_closed_message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['abct_closed_message'] ) ) : ''; update_option( ABCT_Store_Status_Manager::CLOSED_MESSAGE_OPTION, $closed_message ); delete_transient( 'abct_store_state_cache' ); wp_safe_redirect( add_query_arg( array( 'page' => 'abct-edit-schedule', 'abct_settings_saved' => 1, ), admin_url( 'admin.php' ) ) ); exit; } /** * Toggle manual mode over AJAX. * * @return void */ public function ajax_toggle_manual_mode() { if ( ! $this->can_manage_schedule() ) { wp_send_json_error( array( 'message' => __( 'Permission denied.', 'abct-smart-orders' ) ), 403 ); } check_ajax_referer( 'abct_manual_mode_nonce', 'nonce' ); $next = 'offline' === $this->status_manager->get_manual_mode() ? 'online' : 'offline'; $this->status_manager->set_manual_mode( $next ); delete_transient( 'abct_store_state_cache' ); wp_send_json_success( array( 'manual_mode' => $this->status_manager->get_manual_mode(), 'state' => $this->status_manager->get_state(), ) ); } /** * Check schedule management capability. * * @return bool */ private function can_manage_schedule() { return current_user_can( 'manage_woocommerce' ) || current_user_can( 'manage_options' ); } /** * Validate whether the configured shifts produce a clear daily schedule. * * @param array $schedule Sanitized schedule payload. * @return string Error code, or empty string when valid. */ private function validate_schedule_configuration( $schedule ) { $schedule = is_array( $schedule ) ? $schedule : array(); $open_24_hours = ! empty( $schedule['open_24_hours'] ); $lunch_enabled = ! empty( $schedule['lunch_enabled'] ); $dinner_enabled = ! empty( $schedule['dinner_enabled'] ); if ( ! $open_24_hours && ! $lunch_enabled && ! $dinner_enabled ) { return 'no_schedule_selected'; } $weekdays = isset( $schedule['weekdays'] ) && is_array( $schedule['weekdays'] ) ? $schedule['weekdays'] : array(); $has_weekday_selected = false; foreach ( array( 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun' ) as $weekday ) { if ( ! empty( $weekdays[ $weekday ] ) ) { $has_weekday_selected = true; break; } } if ( ! $has_weekday_selected ) { return 'no_weekday_selected'; } // When 24-hour mode is active, weekdays define the allowed days and shifts are only preserved for later use. if ( $open_24_hours ) { return ''; } $turno_1_error = $this->validate_shift_window( 'lunch', $lunch_enabled, isset( $schedule['lunch_open'] ) ? (string) $schedule['lunch_open'] : '', isset( $schedule['lunch_close'] ) ? (string) $schedule['lunch_close'] : '' ); if ( '' !== $turno_1_error ) { return $turno_1_error; } $turno_2_error = $this->validate_shift_window( 'dinner', $dinner_enabled, isset( $schedule['dinner_open'] ) ? (string) $schedule['dinner_open'] : '', isset( $schedule['dinner_close'] ) ? (string) $schedule['dinner_close'] : '' ); if ( '' !== $turno_2_error ) { return $turno_2_error; } $turno_1 = $this->build_daily_shift_window( 'lunch', ! empty( $schedule['lunch_enabled'] ), isset( $schedule['lunch_open'] ) ? (string) $schedule['lunch_open'] : '', isset( $schedule['lunch_close'] ) ? (string) $schedule['lunch_close'] : '' ); $turno_2 = $this->build_daily_shift_window( 'dinner', ! empty( $schedule['dinner_enabled'] ), isset( $schedule['dinner_open'] ) ? (string) $schedule['dinner_open'] : '', isset( $schedule['dinner_close'] ) ? (string) $schedule['dinner_close'] : '' ); if ( null === $turno_1 || null === $turno_2 ) { return ''; } $overlap_start = max( $turno_1['start'], $turno_2['start'] ); $overlap_end = min( $turno_1['end'], $turno_2['end'] ); if ( $overlap_start < $overlap_end ) { return 'shift_conflict'; } if ( $turno_2['start'] <= $turno_1['end'] ) { return 'shift_order_invalid'; } return ''; } /** * Validate a shift against the fixed daily range rules. * * @param string $shift_key Shift identifier. * @param bool $enabled Whether the shift is enabled. * @param string $open_time Open time HH:MM. * @param string $close_time Close time HH:MM. * @return string */ private function validate_shift_window( $shift_key, $enabled, $open_time, $close_time ) { if ( ! $enabled ) { return ''; } $start = $this->parse_time_to_minutes( $open_time ); $end = $this->parse_close_time_to_minutes( $close_time ); if ( null === $start || null === $end || $start >= $end ) { return 'lunch' === $shift_key ? 'shift_1_invalid' : 'shift_2_invalid'; } return ''; } /** * Build the operational window for a valid shift inside a single calendar day. * * @param string $shift_key Shift identifier. * @param bool $enabled Whether the shift is enabled. * @param string $open_time Open time HH:MM. * @param string $close_time Close time HH:MM. * @return array{start:int,end:int}|null */ private function build_daily_shift_window( $shift_key, $enabled, $open_time, $close_time ) { if ( ! $enabled ) { return null; } $start = $this->parse_time_to_minutes( $open_time ); $end = $this->parse_close_time_to_minutes( $close_time ); if ( null === $start || null === $end || '' !== $this->validate_shift_window( $shift_key, true, $open_time, $close_time ) ) { return null; } return array( 'start' => $start, 'end' => $end, ); } /** * Return the first valid Shift 2 start time after Shift 1 closes. * * @param string $time Shift 1 close time. * @return string */ private function get_next_minute_time( $time ) { $minutes = $this->parse_time_to_minutes( $time ); if ( null === $minutes || $minutes >= 1439 ) { return '00:00'; } ++$minutes; return sprintf( '%02d:%02d', (int) floor( $minutes / 60 ), (int) ( $minutes % 60 ) ); } /** * Render same-day time options for Shift 2, hiding invalid previous times. * * @param string $selected Selected value. * @param string $min_time Minimum allowed start time. * @return string */ private function render_time_options( $selected, $min_time ) { $start = $this->parse_time_to_minutes( $min_time ); if ( null === $start ) { $start = 0; } $output = ''; for ( $minute = $start; $minute <= 1439; ++$minute ) { $value = sprintf( '%02d:%02d', (int) floor( $minute / 60 ), (int) ( $minute % 60 ) ); $output .= sprintf( '', esc_attr( $value ), selected( $selected, $value, false ) ); } return $output; } /** * Parse a HH:MM string into minutes from midnight. * * @param string $time Time string. * @return int|null */ private function parse_time_to_minutes( $time ) { $time = trim( (string) $time ); if ( ! preg_match( '/^(?:[01]\d|2[0-3]):[0-5]\d$/', $time ) ) { return null; } list( $hours, $minutes ) = array_map( 'intval', explode( ':', $time ) ); return ( $hours * 60 ) + $minutes; } /** * Parse a close time, treating 00:00 as end of day for schedule validation. * * @param string $time Time string. * @return int|null */ private function parse_close_time_to_minutes( $time ) { $minutes = $this->parse_time_to_minutes( $time ); if ( null === $minutes ) { return null; } return '00:00' === trim( (string) $time ) ? 1440 : $minutes; } /** * Resolve a translated validation message for the schedule screen. * * @param string $error_code Error code. * @return string */ private function get_schedule_validation_error_message( $error_code ) { if ( 'manual_offline_blocked' === $error_code ) { return __( 'The store is manually closed. Turn it on before changing and saving the schedule.', 'abct-smart-orders' ); } if ( 'no_schedule_selected' === $error_code ) { return __( 'Choose at least one scheduling mode: 24 hours, Shift 1, or Shift 2.', 'abct-smart-orders' ); } if ( 'no_weekday_selected' === $error_code ) { return __( 'Select at least one weekday when the store will be open.', 'abct-smart-orders' ); } if ( 'shift_1_invalid' === $error_code || 'turno_1_invalid' === $error_code ) { return __( 'Shift 1 must use a same-day time range, with the start time earlier than the end time. Overnight shifts are not supported.', 'abct-smart-orders' ); } if ( 'shift_2_invalid' === $error_code || 'turno_2_invalid' === $error_code ) { return __( 'Shift 2 must start after Shift 1 closes and must use a same-day time range. Overnight shifts are not supported.', 'abct-smart-orders' ); } if ( 'shift_conflict' === $error_code ) { return __( 'The configured shifts for this day conflict. Review the times before saving.', 'abct-smart-orders' ); } return __( 'Unable to save the schedule. Review the configuration and try again.', 'abct-smart-orders' ); } }