settings = $settings; $this->syncRunner = $syncRunner; $this->progress = $progress; $this->agents = $agents; $this->pluginPath = $pluginPath; $this->pluginUrl = $pluginUrl; $this->version = $version; $this->onboarding = $onboarding; } public function register(): void { \add_action( 'admin_menu', [ $this, 'addMenuPage' ] ); \add_action( 'admin_post_' . self::SAVE_ACTION, [ $this, 'handleSubmit' ] ); \add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ] ); } public function addMenuPage(): void { \add_menu_page( 'Agent001', 'Agent001', self::CAPABILITY, self::PAGE_SLUG, [ $this, 'render' ], 'dashicons-format-chat' ); } public function enqueueAssets( string $hook ): void { if ( $hook !== 'toplevel_page_' . self::PAGE_SLUG ) { return; } \wp_enqueue_style( 'agent001-sync', $this->pluginUrl . 'assets/css/agent001-sync.css', [], $this->version ); \wp_enqueue_script( 'agent001-sync', $this->pluginUrl . 'assets/js/agent001-sync.js', [], $this->version, true ); \wp_enqueue_script( 'agent001-categories', $this->pluginUrl . 'assets/js/agent001-categories.js', [], $this->version . '.' . (string) \filemtime( $this->pluginPath . 'assets/js/agent001-categories.js' ), true ); \wp_localize_script( 'agent001-sync', 'Agent001Sync', [ 'ajaxUrl' => \admin_url( 'admin-ajax.php' ), 'nonce' => \wp_create_nonce( SyncAjaxController::NONCE ), 'actions' => [ 'start' => SyncAjaxController::START_ACTION, 'status' => SyncAjaxController::STATUS_ACTION, ], 'pollInterval' => 5000, 'i18n' => [ 'syncNow' => \__( 'Sync now', 'agent001-support-assistant-for-woocommerce' ), 'syncing' => \__( 'Syncing…', 'agent001-support-assistant-for-woocommerce' ), 'idle' => \__( 'Idle', 'agent001-support-assistant-for-woocommerce' ), 'queued' => \__( 'Queued…', 'agent001-support-assistant-for-woocommerce' ), 'reading' => \__( 'Reading products…', 'agent001-support-assistant-for-woocommerce' ), 'posting' => \__( 'Sending to API…', 'agent001-support-assistant-for-woocommerce' ), 'indexing' => \__( 'Saving…', 'agent001-support-assistant-for-woocommerce' ), 'indexingHint' => \__( 'Large catalogs can take several minutes.', 'agent001-support-assistant-for-woocommerce' ), 'done' => \__( 'Sync completed.', 'agent001-support-assistant-for-woocommerce' ), 'error' => \__( 'Sync failed.', 'agent001-support-assistant-for-woocommerce' ), 'conflict' => \__( 'Another sync is already running.', 'agent001-support-assistant-for-woocommerce' ), 'network' => \__( 'Network error. Retrying…', 'agent001-support-assistant-for-woocommerce' ), ], ] ); } public function render(): void { if ( ! \current_user_can( self::CAPABILITY ) ) { return; } $apiKeyLocked = $this->settings->apiKeyIsLocked(); $isConnected = $this->settings->hasApiKey(); $connectedSite = $this->settings->connectedSite(); $accountName = $this->settings->accountName(); $progress = $this->progress->current(); $selectedCatIds = $this->settings->syncCategories(); $availableCategories = $this->fetchAvailableCategories(); $availableAgents = []; $selectedAgentIds = $this->settings->selectedAgentIds(); $agentsFetchFailed = false; if ( $isConnected ) { $cached = \get_transient( AgentSelectionController::AGENTS_TRANSIENT ); $cacheHasAgentStatus = is_array( $cached ) && isset( $cached['agents'] ) && is_array( $cached['agents'] ) && array_filter( $cached['agents'], static fn( $agent ): bool => ! is_array( $agent ) || ! array_key_exists( 'active', $agent ) ) === []; if ( is_array( $cached ) && isset( $cached['agents'] ) && ! empty( $cached['account_name'] ) && $cacheHasAgentStatus ) { $availableAgents = $cached['agents']; $cachedIds = is_array( $cached['selected_ids'] ?? null ) ? AgentId::normalizeList( $cached['selected_ids'] ) : []; $selectedAgentIds = $cachedIds !== [] ? $cachedIds : $selectedAgentIds; } else { $agentsPayload = $this->agents->fetchAgents(); if ( $agentsPayload === null ) { $agentsFetchFailed = true; } else { $availableAgents = $agentsPayload['agents']; $selectedAgentIds = $agentsPayload['selected_ids'] !== [] ? $agentsPayload['selected_ids'] : $selectedAgentIds; } } } $isOnboarding = $this->onboarding->isActive(); $onboardingPaused = $this->onboarding->isPaused(); $setupStep = $this->onboarding->step(); $onboarding = $this->onboarding; $backendUrl = $this->settings->backendBaseUrl(); echo '

Agent001

'; echo '
'; echo '

' . \esc_html__( 'Chat not visible on your store? Clear your site cache.', 'agent001-support-assistant-for-woocommerce' ) . '

'; echo '

' . \esc_html__( 'Agent001 clears the cache of popular caching plugins automatically. If your store also uses a hosting cache or a CDN such as Cloudflare, clear it manually after finishing setup or changing the agent, then reload your store page.', 'agent001-support-assistant-for-woocommerce' ) . '

'; echo '
'; \settings_errors( 'agent001' ); echo '
'; echo ''; \wp_nonce_field( self::SAVE_ACTION, '_agent001_nonce' ); include $this->pluginPath . 'src/Admin/views/settings-page.php'; if ( ! $isOnboarding && $isConnected ) { \submit_button( \__( 'Save changes', 'agent001-support-assistant-for-woocommerce' ) ); } echo '
'; } public function handleSubmit(): void { if ( ! \current_user_can( self::CAPABILITY ) ) { \wp_die( \esc_html__( 'Permission denied.', 'agent001-support-assistant-for-woocommerce' ), '', [ 'response' => 403 ] ); } if ( isset( $_POST['agent001_toggle_agent'] ) ) { $this->handleAgentToggle(); return; } $this->handleSave(); if ( \get_settings_errors( 'agent001' ) === [] ) { \add_settings_error( 'agent001', 'saved', \__( 'Settings saved.', 'agent001-support-assistant-for-woocommerce' ), 'success' ); } \set_transient( 'settings_errors', \get_settings_errors(), 30 ); \wp_safe_redirect( \add_query_arg( 'settings-updated', 'true', $this->onboarding->settingsUrl() ) ); exit; } private function handleAgentToggle(): void { \check_admin_referer( self::SAVE_ACTION, '_agent001_nonce' ); $rawAgentId = isset( $_POST['agent001_toggle_agent'] ) && is_scalar( $_POST['agent001_toggle_agent'] ) ? \sanitize_text_field( \wp_unslash( (string) $_POST['agent001_toggle_agent'] ) ) : ''; $rawActive = isset( $_POST['agent001_toggle_active'] ) && is_scalar( $_POST['agent001_toggle_active'] ) ? (string) \wp_unslash( $_POST['agent001_toggle_active'] ) : ''; $toggleAgentId = AgentId::normalize( $rawAgentId ); if ( $toggleAgentId === '' || ! in_array( $rawActive, [ '0', '1' ], true ) ) { \add_settings_error( 'agent001', 'agent_toggle_error', \__( 'Invalid agent status update.', 'agent001-support-assistant-for-woocommerce' ) ); } else { $error = $this->agents->setAgentActive( $toggleAgentId, $rawActive === '1' ); if ( $error !== null ) { \add_settings_error( 'agent001', 'agent_toggle_error', $error ); } else { \add_settings_error( 'agent001', 'agent_toggle_success', $rawActive === '1' ? \__( 'Agent enabled.', 'agent001-support-assistant-for-woocommerce' ) : \__( 'Agent disabled.', 'agent001-support-assistant-for-woocommerce' ), 'success' ); } } \set_transient( 'settings_errors', \get_settings_errors(), 30 ); \wp_safe_redirect( $this->onboarding->settingsUrl() ); exit; } /** * @return array */ private function fetchAvailableCategories(): array { if ( ! \taxonomy_exists( 'product_cat' ) ) { return []; } $terms = \get_terms( [ 'taxonomy' => 'product_cat', 'hide_empty' => false, 'orderby' => 'name', ] ); if ( ! is_array( $terms ) ) { return []; } $byParent = []; foreach ( $terms as $term ) { $byParent[ (int) $term->parent ][] = $term; } $ordered = []; $walker = static function ( int $parent, int $depth ) use ( &$walker, &$byParent, &$ordered ): void { if ( ! isset( $byParent[ $parent ] ) ) { return; } foreach ( $byParent[ $parent ] as $term ) { $ordered[] = [ 'id' => (int) $term->term_id, 'name' => (string) $term->name, 'parent' => (int) $term->parent, 'count' => (int) $term->count, 'depth' => $depth, ]; $walker( (int) $term->term_id, $depth + 1 ); } }; $walker( 0, 0 ); return $ordered; } public function handleSave(): void { if ( ! \current_user_can( self::CAPABILITY ) ) { return; } \check_admin_referer( self::SAVE_ACTION, '_agent001_nonce' ); if ( $this->progress->isRunning() ) { \add_settings_error( 'agent001', 'save_error', \__( 'Wait for the current synchronization to finish before changing the agent or products.', 'agent001-support-assistant-for-woocommerce' ) ); return; } $rawPost = isset( $_POST['agent001_settings'] ) && is_array( $_POST['agent001_settings'] ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nested values are sanitized below per type. ? \map_deep( \wp_unslash( $_POST['agent001_settings'] ), 'sanitize_text_field' ) : []; $raw = is_array( $rawPost ) ? $rawPost : []; if ( $this->onboarding->isActive() && $this->onboarding->step() === 1 ) { return; } if ( ! isset( $raw['agent_id'] ) || ! is_scalar( $raw['agent_id'] ) ) { \add_settings_error( 'agent001', 'save_error', \__( 'Select an agent before saving.', 'agent001-support-assistant-for-woocommerce' ) ); return; } $rawCats = isset( $raw['sync_categories'] ) && is_array( $raw['sync_categories'] ) ? $raw['sync_categories'] : []; $rawAgentId = isset( $raw['agent_id'] ) ? (string) $raw['agent_id'] : ''; $selectedId = AgentId::normalize( $rawAgentId ); if ( $rawAgentId !== '' && $selectedId === '' ) { \add_settings_error( 'agent001', 'save_error', \__( 'Invalid agent selection.', 'agent001-support-assistant-for-woocommerce' ) ); return; } $ids = []; foreach ( $rawCats as $value ) { if ( ! is_scalar( $value ) || ! ctype_digit( (string) $value ) ) { \add_settings_error( 'agent001', 'save_error', \__( 'Invalid product category selection.', 'agent001-support-assistant-for-woocommerce' ) ); return; } $id = (int) $value; if ( $id > 0 ) { $ids[] = $id; } } $newCategoryIds = array_values( array_unique( $ids ) ); sort( $newCategoryIds ); $availableCategoryIds = array_column( $this->fetchAvailableCategories(), 'id' ); if ( $availableCategoryIds !== [] && $newCategoryIds === [] ) { \add_settings_error( 'agent001', 'save_error', \__( 'Select at least one product category.', 'agent001-support-assistant-for-woocommerce' ) ); return; } if ( array_diff( $newCategoryIds, $availableCategoryIds ) !== [] ) { \add_settings_error( 'agent001', 'save_error', \__( 'Invalid product category selection.', 'agent001-support-assistant-for-woocommerce' ) ); return; } $previousCategoryIds = $this->settings->syncCategories(); sort( $previousCategoryIds ); $previousAgentIds = $this->settings->selectedAgentIds(); sort( $previousAgentIds ); $agentIds = []; if ( $selectedId !== '' ) { $agentIds[] = $selectedId; } $newAgentIds = array_values( array_unique( $agentIds ) ); sort( $newAgentIds ); if ( $this->onboarding->isActive() && $agentIds === [] ) { \add_settings_error( 'agent001', 'save_error', \__( 'Select an agent to continue setup.', 'agent001-support-assistant-for-woocommerce' ) ); return; } if ( $this->settings->hasApiKey() ) { $error = $this->agents->saveSelection( $agentIds ); if ( $error !== null ) { \add_settings_error( 'agent001', 'save_error', sprintf( /* translators: %s: error message from remote */ \__( 'Agent selection: %s', 'agent001-support-assistant-for-woocommerce' ), $error ) ); return; } } $this->settings->setSyncCategories( $newCategoryIds ); if ( $this->onboarding->isActive() && $this->settings->selectedAgentEmbedToken() === '' ) { \add_settings_error( 'agent001', 'save_error', \__( 'The selected agent has no widget token. Refresh the agent list or check the agent settings in Agent001, then try again.', 'agent001-support-assistant-for-woocommerce' ) ); return; } $selectionChanged = ( $newCategoryIds !== $previousCategoryIds ) || ( $newAgentIds !== $previousAgentIds ); $needsFirstSync = $this->onboarding->isActive() && ! $this->onboarding->canFinish(); if ( ( $selectionChanged || $needsFirstSync ) && $this->settings->hasApiKey() && $this->settings->hasSelectedAgents() && ! $this->progress->isRunning() ) { $this->syncRunner->schedule(); } } }