'skipped', 'message' => 'Brak zmian do synchronizacji.' ]; } $supported_fields = [ 'title', 'description', 'google_product_category' ]; $normalized_changes = []; foreach ( $changed_fields as $field => $change ) { if ( !in_array( $field, $supported_fields, true ) ) { continue; } $old_value = trim( (string) ( $change['old'] ?? '' ) ); $new_value = trim( (string) ( $change['new'] ?? '' ) ); if ( $old_value === $new_value ) { continue; } $normalized_changes[ $field ] = [ 'old' => $old_value, 'new' => $new_value ]; } if ( empty( $normalized_changes ) ) { return [ 'status' => 'skipped', 'message' => 'Brak rzeczywistych zmian pól.' ]; } $merchant_context = \factory\Products::get_product_merchant_context( $product_id ); $merchant_account_id = trim( (string) ( $merchant_context['google_merchant_account_id'] ?? '' ) ); $offer_id = trim( (string) ( $merchant_context['offer_id'] ?? '' ) ); if ( !$merchant_context || $merchant_account_id === '' || $offer_id === '' ) { $reason = 'Brak merchant_account_id lub offer_id dla produktu.'; foreach ( $normalized_changes as $field => $change ) { \factory\Products::add_product_merchant_sync_log( [ 'product_id' => $product_id, 'field_name' => $field, 'old_value' => $change['old'], 'new_value' => $change['new'], 'sync_status' => 'skipped', 'sync_source' => $sync_source, 'merchant_account_id' => $merchant_account_id !== '' ? $merchant_account_id : null, 'offer_id' => $offer_id !== '' ? $offer_id : null, 'error_message' => $reason ] ); } return [ 'status' => 'skipped', 'message' => $reason ]; } $merchant_api = new \services\GoogleAdsApi(); if ( !$merchant_api -> is_merchant_configured() ) { $reason = 'Merchant API nie jest skonfigurowane.'; foreach ( $normalized_changes as $field => $change ) { \factory\Products::add_product_merchant_sync_log( [ 'product_id' => $product_id, 'field_name' => $field, 'old_value' => $change['old'], 'new_value' => $change['new'], 'sync_status' => 'skipped', 'sync_source' => $sync_source, 'merchant_account_id' => $merchant_account_id, 'offer_id' => $offer_id, 'error_message' => $reason ] ); } return [ 'status' => 'skipped', 'message' => $reason ]; } $payload_fields = []; foreach ( $normalized_changes as $field => $change ) { $payload_fields[ $field ] = $change['new']; } $sync_result = $merchant_api -> update_merchant_product_fields_by_offer_id( $merchant_account_id, $offer_id, $payload_fields ); $sync_success = !empty( $sync_result['success'] ); $sync_status = $sync_success ? 'success' : 'error'; $sync_error = trim( (string) ( $sync_result['error'] ?? '' ) ); $merchant_product_id = trim( (string) ( $sync_result['merchant_product_id'] ?? '' ) ); $api_response = null; if ( isset( $sync_result['response'] ) ) { $api_response = is_string( $sync_result['response'] ) ? $sync_result['response'] : json_encode( $sync_result['response'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); } foreach ( $normalized_changes as $field => $change ) { \factory\Products::add_product_merchant_sync_log( [ 'product_id' => $product_id, 'field_name' => $field, 'old_value' => $change['old'], 'new_value' => $change['new'], 'sync_status' => $sync_status, 'sync_source' => $sync_source, 'merchant_account_id' => $merchant_account_id, 'merchant_product_id' => $merchant_product_id !== '' ? $merchant_product_id : null, 'offer_id' => $offer_id, 'api_response' => $api_response, 'error_message' => $sync_error !== '' ? $sync_error : null ] ); } return [ 'status' => $sync_status, 'message' => $sync_success ? 'Synchronizacja Merchant API zakończona sukcesem.' : ( $sync_error !== '' ? $sync_error : 'Błąd synchronizacji Merchant API.' ), 'result' => $sync_result ]; } static public function main_view() { return \Tpl::view( 'products/main_view', [ 'clients' => \factory\Campaigns::get_clients(), ] ); } static public function get_campaigns_list() { $client_id = (int) \S::get( 'client_id' ); $campaigns = \factory\Campaigns::get_campaigns_list( $client_id, true ); $allowed_channel_types = [ 'SHOPPING', 'PERFORMANCE_MAX' ]; $campaigns = array_values( array_filter( (array) $campaigns, function( $row ) use ( $allowed_channel_types ) { $channel_type = strtoupper( trim( (string) ( $row['advertising_channel_type'] ?? '' ) ) ); return in_array( $channel_type, $allowed_channel_types, true ); } ) ); echo json_encode( [ 'campaigns' => $campaigns ] ); exit; } static public function get_campaign_ad_groups() { $campaign_id = (int) \S::get( 'campaign_id' ); if ( $campaign_id <= 0 ) { echo json_encode( [ 'ad_groups' => [] ] ); exit; } echo json_encode( [ 'ad_groups' => \factory\Campaigns::get_campaign_ad_groups( $campaign_id ) ] ); exit; } static public function get_client_bestseller_settings() { $client_id = (int) \S::get( 'client_id' ); if ( $client_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Nieprawidlowe ID klienta.' ] ); exit; } $settings = \factory\Products::get_client_bestseller_settings( $client_id ); echo json_encode( [ 'status' => 'ok', 'settings' => $settings ] ); exit; } static public function save_client_bestseller_settings() { $client_id = (int) \S::get( 'client_id' ); if ( $client_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Nieprawidlowe ID klienta.' ] ); exit; } $settings = [ 'bestseller_roas_entry' => \S::get( 'bestseller_roas_entry' ), 'bestseller_roas_exit' => \S::get( 'bestseller_roas_exit' ), 'min_conversions' => \S::get( 'min_conversions' ), 'cooldown_period' => \S::get( 'cooldown_period' ) ]; $saved = \factory\Products::save_client_bestseller_settings( $client_id, $settings ); echo json_encode( [ 'status' => $saved ? 'ok' : 'error', 'settings' => \factory\Products::get_client_bestseller_settings( $client_id ) ] ); exit; } static public function preview_client_bestseller_settings() { $client_id = (int) \S::get( 'client_id' ); if ( $client_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Nieprawidlowe ID klienta.' ] ); exit; } $settings = [ 'bestseller_roas_entry' => \S::get( 'bestseller_roas_entry' ), 'bestseller_roas_exit' => \S::get( 'bestseller_roas_exit' ), 'min_conversions' => \S::get( 'min_conversions' ), 'cooldown_period' => \S::get( 'cooldown_period' ) ]; $count = \factory\Products::get_client_bestseller_preview_count( $client_id, $settings ); echo json_encode( [ 'status' => 'ok', 'count' => (int) $count ] ); exit; } static public function get_scope_alerts() { $client_id = (int) \S::get( 'client_id' ); $campaign_id = (int) \S::get( 'campaign_id' ); $ad_group_id = (int) \S::get( 'ad_group_id' ); $alerts = \factory\Products::get_scope_alerts( $client_id, $campaign_id, $ad_group_id, 80 ); echo json_encode( [ 'status' => 'ok', 'alerts' => $alerts, 'count' => count( $alerts ) ] ); exit; } static public function get_products_without_impressions_30() { $client_id = (int) \S::get( 'client_id' ); $campaign_id = (int) \S::get( 'campaign_id' ); $ad_group_id = (int) \S::get( 'ad_group_id' ); if ( $client_id <= 0 || $campaign_id <= 0 ) { echo json_encode( [ 'status' => 'ok', 'products' => [], 'count' => 0 ] ); exit; } $rows = \factory\Products::get_products_without_impressions_30( $client_id, $campaign_id, $ad_group_id, 1000 ); $products = []; foreach ( (array) $rows as $row ) { $product_id = (int) ( $row['product_id'] ?? 0 ); if ( $product_id <= 0 ) { continue; } $offer_id = trim( (string) ( $row['offer_id'] ?? '' ) ); $product_name = trim( (string) ( $row['title'] ?? '' ) ); if ( $product_name === '' ) { $product_name = trim( (string) ( $row['name'] ?? '' ) ); } if ( $product_name === '' ) { $product_name = $offer_id !== '' ? $offer_id : ( 'Produkt #' . $product_id ); } $products[] = [ 'product_id' => $product_id, 'offer_id' => $offer_id, 'name' => $product_name ]; } echo json_encode( [ 'status' => 'ok', 'products' => $products, 'count' => count( $products ) ] ); exit; } static public function delete_campaign_ad_group() { $campaign_id = (int) \S::get( 'campaign_id' ); $ad_group_id = (int) \S::get( 'ad_group_id' ); $delete_scope = trim( (string) \S::get( 'delete_scope' ) ); if ( $ad_group_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Nie wybrano grupy reklam do usuniecia.' ] ); exit; } if ( !in_array( $delete_scope, [ 'local', 'google' ], true ) ) { $delete_scope = 'local'; } $context = \factory\Products::get_ad_group_delete_context( $ad_group_id ); if ( !$context ) { echo json_encode( [ 'status' => 'ok', 'message' => 'Grupa reklam byla juz usunieta lokalnie.' ] ); exit; } $local_campaign_id = (int) ( $context['local_campaign_id'] ?? 0 ); if ( $campaign_id > 0 && $campaign_id !== $local_campaign_id ) { echo json_encode( [ 'status' => 'error', 'message' => 'Wybrana grupa reklam nie nalezy do wskazanej kampanii.' ] ); exit; } $channel_type = strtoupper( trim( (string) ( $context['advertising_channel_type'] ?? '' ) ) ); if ( $channel_type !== 'SHOPPING' ) { echo json_encode( [ 'status' => 'error', 'message' => 'Usuwanie grup reklam jest dostepne tylko dla kampanii produktowych (Shopping).' ] ); exit; } if ( $delete_scope === 'google' ) { $customer_id = preg_replace( '/\D+/', '', (string) ( $context['google_ads_customer_id'] ?? '' ) ); $external_ad_group_id = (int) ( $context['external_ad_group_id'] ?? 0 ); if ( $customer_id === '' || $external_ad_group_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Brak danych Google Ads (customer_id lub ad_group_id).' ] ); exit; } $google_ads_api = new \services\GoogleAdsApi(); if ( !$google_ads_api -> is_configured() ) { echo json_encode( [ 'status' => 'error', 'message' => 'Google Ads API nie jest skonfigurowane.' ] ); exit; } $google_result = $google_ads_api -> remove_ad_group( $customer_id, $external_ad_group_id ); if ( empty( $google_result['success'] ) ) { $error_message = trim( (string) ( $google_result['error'] ?? \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ) ) ); if ( $error_message === '' ) { $error_message = 'Nie udalo sie usunac grupy reklam w Google Ads.'; } echo json_encode( [ 'status' => 'error', 'message' => $error_message ] ); exit; } } if ( !\factory\Products::delete_ad_group_local( $ad_group_id ) ) { $still_exists_local = \factory\Products::get_ad_group_scope_context( $ad_group_id ); if ( !$still_exists_local ) { echo json_encode( [ 'status' => 'ok', 'message' => 'Grupa reklam zostala usunieta lokalnie.' ] ); exit; } echo json_encode( [ 'status' => 'error', 'message' => 'Nie udalo sie usunac grupy reklam lokalnie.' ] ); exit; } if ( $delete_scope === 'google' ) { echo json_encode( [ 'status' => 'ok', 'message' => 'Usunieto grupe reklam lokalnie oraz w Google Ads.' ] ); } else { echo json_encode( [ 'status' => 'ok', 'message' => 'Usunieto grupe reklam lokalnie.' ] ); } exit; } static public function assign_product_scope() { $product_id = (int) \S::get( 'product_id' ); $campaign_mode = trim( (string) \S::get( 'campaign_mode' ) ); $campaign_id = (int) \S::get( 'campaign_id' ); $campaign_name = trim( (string) \S::get( 'campaign_name' ) ); $ad_group_mode = trim( (string) \S::get( 'ad_group_mode' ) ); $ad_group_id = (int) \S::get( 'ad_group_id' ); $ad_group_name = trim( (string) \S::get( 'ad_group_name' ) ); $campaign_daily_budget = (float) \S::get( 'campaign_daily_budget' ); $default_cpc = (float) \S::get( 'default_cpc' ); if ( $campaign_daily_budget <= 0 ) { $campaign_daily_budget = 50.0; } if ( $default_cpc <= 0 ) { $default_cpc = 1.0; } if ( $product_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Nieprawidłowy produkt.' ] ); exit; } $product_context = \factory\Products::get_product_scope_context( $product_id ); if ( !$product_context ) { echo json_encode( [ 'status' => 'error', 'message' => 'Nie znaleziono produktu.' ] ); exit; } $client_id = (int) ( $product_context['client_id'] ?? 0 ); $offer_id = trim( (string) ( $product_context['offer_id'] ?? '' ) ); $customer_id = preg_replace( '/\D+/', '', (string) ( $product_context['google_ads_customer_id'] ?? '' ) ); $merchant_account_id = preg_replace( '/\D+/', '', (string) ( $product_context['google_merchant_account_id'] ?? '' ) ); if ( $offer_id === '' ) { echo json_encode( [ 'status' => 'error', 'message' => 'Produkt nie ma offer_id (ID oferty).' ] ); exit; } if ( $customer_id === '' ) { echo json_encode( [ 'status' => 'error', 'message' => 'Brak Google Ads Customer ID u klienta.' ] ); exit; } $google_ads_api = new \services\GoogleAdsApi(); if ( !$google_ads_api -> is_configured() ) { echo json_encode( [ 'status' => 'error', 'message' => 'Google Ads API nie jest skonfigurowane.' ] ); exit; } $external_campaign_id = 0; $external_ad_group_id = 0; $resolved_campaign_name = ''; $resolved_ad_group_name = ''; if ( $campaign_mode === 'new' ) { if ( $campaign_name === '' ) { echo json_encode( [ 'status' => 'error', 'message' => 'Podaj nazwę nowej kampanii.' ] ); exit; } if ( $merchant_account_id === '' ) { echo json_encode( [ 'status' => 'error', 'message' => 'Brak Google Merchant Account ID u klienta (wymagane dla kampanii Shopping).' ] ); exit; } $campaign_result = $google_ads_api -> create_standard_shopping_campaign( $customer_id, $merchant_account_id, $campaign_name, $campaign_daily_budget ); if ( empty( $campaign_result['success'] ) ) { $error_message = trim( (string) ( $campaign_result['error'] ?? \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ) ) ); echo json_encode( [ 'status' => 'error', 'message' => $error_message !== '' ? $error_message : 'Nie udało się utworzyć kampanii Standard Shopping w Google Ads.' ] ); exit; } $external_campaign_id = (int) ( $campaign_result['campaign_id'] ?? 0 ); $resolved_campaign_name = trim( (string) ( $campaign_result['campaign_name'] ?? $campaign_name ) ); } else { if ( $campaign_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Wybierz kampanię.' ] ); exit; } $campaign_scope = \factory\Products::get_campaign_scope_context( $campaign_id ); if ( !$campaign_scope ) { echo json_encode( [ 'status' => 'error', 'message' => 'Nie znaleziono wybranej kampanii.' ] ); exit; } if ( (int) ( $campaign_scope['client_id'] ?? 0 ) !== $client_id ) { echo json_encode( [ 'status' => 'error', 'message' => 'Wybrana kampania nie należy do klienta produktu.' ] ); exit; } $external_campaign_id = (int) ( $campaign_scope['campaign_id'] ?? 0 ); $resolved_campaign_name = trim( (string) ( $campaign_scope['campaign_name'] ?? '' ) ); if ( $external_campaign_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Wybrana kampania nie ma ID Google Ads. Wybierz kampanię zsynchronizowaną z Google Ads.' ] ); exit; } } if ( $ad_group_mode === 'new' ) { if ( $ad_group_name === '' ) { echo json_encode( [ 'status' => 'error', 'message' => 'Podaj nazwę nowej grupy reklam.' ] ); exit; } $ad_group_result = $google_ads_api -> create_standard_shopping_ad_group_with_offer( $customer_id, $external_campaign_id, $ad_group_name, $offer_id, $default_cpc ); if ( empty( $ad_group_result['success'] ) ) { $error_message = trim( (string) ( $ad_group_result['error'] ?? \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ) ) ); echo json_encode( [ 'status' => 'error', 'message' => $error_message !== '' ? $error_message : 'Nie udało się utworzyć grupy reklam i przypisać produktu w Google Ads.' ] ); exit; } $external_ad_group_id = (int) ( $ad_group_result['ad_group_id'] ?? 0 ); $resolved_ad_group_name = trim( (string) ( $ad_group_result['ad_group_name'] ?? $ad_group_name ) ); } else { if ( $ad_group_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Wybierz grupę reklam.' ] ); exit; } $ad_group_scope = \factory\Products::get_ad_group_scope_context( $ad_group_id ); if ( !$ad_group_scope ) { echo json_encode( [ 'status' => 'error', 'message' => 'Nie znaleziono wybranej grupy reklam.' ] ); exit; } if ( (int) ( $ad_group_scope['campaign_id'] ?? 0 ) !== (int) $campaign_id && $campaign_mode !== 'new' ) { echo json_encode( [ 'status' => 'error', 'message' => 'Wybrana grupa reklam nie należy do wskazanej kampanii.' ] ); exit; } $external_ad_group_id = (int) ( $ad_group_scope['ad_group_id'] ?? 0 ); $resolved_ad_group_name = trim( (string) ( $ad_group_scope['ad_group_name'] ?? '' ) ); if ( $external_ad_group_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Wybrana grupa reklam nie ma ID Google Ads. Wybierz grupę zsynchronizowaną z Google Ads.' ] ); exit; } $offer_result = $google_ads_api -> ensure_standard_shopping_offer_in_ad_group( $customer_id, $external_ad_group_id, $offer_id, $default_cpc ); if ( empty( $offer_result['success'] ) ) { $error_message = trim( (string) ( $offer_result['error'] ?? \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ) ) ); echo json_encode( [ 'status' => 'error', 'message' => $error_message !== '' ? $error_message : 'Nie udało się przypisać produktu do wybranej grupy reklam w Google Ads.' ] ); exit; } } if ( $external_campaign_id <= 0 || $external_ad_group_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Nie udało się przygotować docelowego scope Google Ads.' ] ); exit; } $resolved_scope = \controls\Cron::resolve_products_scope_ids( $client_id, $external_campaign_id, $resolved_campaign_name, $external_ad_group_id, $resolved_ad_group_name, date( 'Y-m-d' ) ); $local_campaign_id = (int) ( $resolved_scope['campaign_id'] ?? 0 ); $local_ad_group_id = (int) ( $resolved_scope['ad_group_id'] ?? 0 ); if ( $local_campaign_id <= 0 || $local_ad_group_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Utworzono scope w Google Ads, ale nie udało się zsynchronizować mapowania lokalnego.' ] ); exit; } if ( !\factory\Products::assign_product_scope( $product_id, $local_campaign_id, $local_ad_group_id ) ) { echo json_encode( [ 'status' => 'error', 'message' => 'Produkt dodano do Google Ads, ale nie udało się zapisać przypisania lokalnie.' ] ); exit; } \factory\Products::add_product_comment( $product_id, 'Przypisano produkt do Google Ads: kampania #' . $external_campaign_id . ' (' . $resolved_campaign_name . '), grupa reklam #' . $external_ad_group_id . ' (' . $resolved_ad_group_name . ').' ); echo json_encode( [ 'status' => 'ok', 'campaign_id' => $local_campaign_id, 'ad_group_id' => $local_ad_group_id, 'external_campaign_id' => $external_campaign_id, 'external_ad_group_id' => $external_ad_group_id ] ); exit; } static public function comment_add() { $product_id = \S::get( 'product_id' ); $date = \S::get( 'date' ); $comment = \S::get( 'comment' ); if ( \factory\Products::add_product_comment( $product_id, $comment, $date ) ) { echo json_encode( [ 'status' => 'ok' ] ); } else echo json_encode( [ 'status' => 'error' ] ); exit; } static public function comment_delete() { $comment_id = \S::get( 'comment_id' ); if ( \factory\Products::delete_product_comment( $comment_id ) ) { echo json_encode( [ 'status' => 'ok' ] ); } else echo json_encode( [ 'status' => 'error' ] ); exit; } static public function get_product_data() { $product_id = \S::get( 'product_id' ); $product_name = \factory\Products::get_product_name( $product_id ); $product_title = \factory\Products::get_product_data( $product_id, 'title' ); $product_description = \factory\Products::get_product_data( $product_id, 'description' ); $google_product_category = \factory\Products::get_product_data( $product_id, 'google_product_category' ); $product_url = \factory\Products::get_product_data( $product_id, 'product_url' ); echo json_encode( [ 'status' => 'ok', 'product_details' => [ 'name' => $product_name, 'title' => $product_title, 'description' => $product_description, 'google_product_category' => $google_product_category, 'product_url' => $product_url ] ] ); exit; } static public function get_product_merchant_sync_logs() { $product_id = (int) \S::get( 'product_id' ); $limit = (int) \S::get( 'limit' ); if ( $product_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Nieprawidłowe ID produktu.' ] ); exit; } if ( $limit <= 0 ) { $limit = 50; } $logs = \factory\Products::get_product_merchant_sync_logs( $product_id, $limit ); echo json_encode( [ 'status' => 'ok', 'logs' => $logs ] ); exit; } static public function delete_product_merchant_sync_log() { $log_id = (int) \S::get( 'log_id' ); if ( $log_id <= 0 ) { echo json_encode( [ 'status' => 'error', 'message' => 'Nieprawidłowe ID logu.' ] ); exit; } $result = \factory\Products::delete_product_merchant_sync_log( $log_id ); echo json_encode( [ 'status' => $result ? 'ok' : 'error' ] ); exit; } static public function ai_suggest() { $product_id = \S::get( 'product_id' ); $field = \S::get( 'field' ); $provider = \S::get( 'provider' ) ?: 'openai'; if ( $provider === 'claude' ) { if ( \services\GoogleAdsApi::get_setting( 'claude_enabled' ) === '0' ) { echo json_encode( [ 'status' => 'error', 'message' => 'Claude jest wyłączony. Włącz go w Ustawieniach.' ] ); exit; } if ( !\services\ClaudeApi::is_configured() ) { echo json_encode( [ 'status' => 'error', 'message' => 'Klucz API Claude nie jest skonfigurowany. Przejdź do Ustawień.' ] ); exit; } } else if ( $provider === 'gemini' ) { if ( \services\GoogleAdsApi::get_setting( 'gemini_enabled' ) === '0' ) { echo json_encode( [ 'status' => 'error', 'message' => 'Gemini jest wyłączony. Włącz go w Ustawieniach.' ] ); exit; } if ( !\services\GeminiApi::is_configured() ) { echo json_encode( [ 'status' => 'error', 'message' => 'Klucz API Gemini nie jest skonfigurowany. Przejdź do Ustawień.' ] ); exit; } } else { if ( \services\GoogleAdsApi::get_setting( 'openai_enabled' ) === '0' ) { echo json_encode( [ 'status' => 'error', 'message' => 'OpenAI jest wyłączony. Włącz go w Ustawieniach.' ] ); exit; } if ( !\services\OpenAiApi::is_configured() ) { echo json_encode( [ 'status' => 'error', 'message' => 'Klucz API OpenAI nie jest skonfigurowany. Przejdź do Ustawień.' ] ); exit; } } $product = \factory\Products::get_product_full_context( $product_id ); if ( !$product ) { echo json_encode( [ 'status' => 'error', 'message' => 'Nie znaleziono produktu.' ] ); exit; } // Pobierz treść strony produktu jeśli podano URL $product_url = \S::get( 'product_url' ); $keyword_source_url = self::normalize_keyword_source_url( $product_url ); $page_content = ''; if ( $product_url && filter_var( $product_url, FILTER_VALIDATE_URL ) ) { $page_content = \services\OpenAiApi::fetch_page_content( $product_url ); } $keyword_terms = []; $warnings = []; $should_enrich_with_keyword_planner = in_array( $field, [ 'title', 'description' ], true ) && in_array( $provider, [ 'openai', 'claude', 'gemini' ], true ); if ( $should_enrich_with_keyword_planner && $keyword_source_url !== '' && filter_var( $keyword_source_url, FILTER_VALIDATE_URL ) ) { $keyword_terms = \factory\Products::get_cached_keyword_planner_terms( $product_id, $keyword_source_url, 15 ); if ( empty( $keyword_terms ) ) { $ads_context = \factory\Products::get_product_ads_keyword_context( $product_id ); $customer_id = trim( (string) ( $ads_context['google_ads_customer_id'] ?? '' ) ); if ( $customer_id !== '' ) { $google_ads_api = new \services\GoogleAdsApi(); $fetched_terms = $google_ads_api -> generate_keyword_ideas_from_url( $customer_id, $keyword_source_url, 40 ); if ( $fetched_terms !== false ) { \factory\Products::replace_keyword_planner_terms( $product_id, $keyword_source_url, $customer_id, $fetched_terms ); $keyword_terms = \factory\Products::get_cached_keyword_planner_terms( $product_id, $keyword_source_url, 15 ); } else { $last_error = trim( (string) \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ) ); $warnings[] = 'Nie udało się pobrać fraz z Google Ads Keyword Planner. ' . ( $last_error !== '' ? 'Szczegóły: ' . $last_error : '' ); } } else { $warnings[] = 'Brak Google Ads Customer ID u klienta — pominięto frazy z Keyword Planner.'; } } } $context = [ 'original_name' => $product['name'], 'current_title' => \factory\Products::get_product_data( $product_id, 'title' ), 'current_description' => \factory\Products::get_product_data( $product_id, 'description' ), 'current_category' => \factory\Products::get_product_data( $product_id, 'google_product_category' ), 'offer_id' => $product['offer_id'], 'impressions_30' => $product['impressions_30'] ?? 0, 'clicks_30' => $product['clicks_30'] ?? 0, 'ctr' => $product['ctr'] ?? 0, 'cost' => $product['cost'] ?? 0, 'conversions' => $product['conversions'] ?? 0, 'conversions_value' => $product['conversions_value'] ?? 0, 'roas' => $product['roas'] ?? 0, 'custom_label_4' => \factory\Products::get_product_data( $product_id, 'custom_label_4' ), 'page_content' => $page_content, 'keyword_planner_terms' => $keyword_terms, ]; $api_map = [ 'claude' => \services\ClaudeApi::class, 'gemini' => \services\GeminiApi::class, ]; $api = $api_map[ $provider ] ?? \services\OpenAiApi::class; switch ( $field ) { case 'title': $result = $api::suggest_title( $context ); break; case 'description': $result = $api::suggest_description( $context ); break; case 'category': $result = $api::suggest_category( $context ); break; default: $result = [ 'status' => 'error', 'message' => 'Nieznane pole: ' . $field ]; } $result['provider'] = $provider; if ( $product_url && !$page_content ) $warnings[] = 'Nie udało się pobrać treści ze strony produktu (strona może blokować dostęp). Sugestia oparta tylko na nazwie produktu.'; elseif ( $page_content ) $result['page_fetched'] = true; if ( !empty( $warnings ) ) $result['warning'] = implode( ' ', array_filter( $warnings ) ); if ( !empty( $keyword_terms ) ) { $result['keyword_planner_terms_used'] = true; $result['keyword_planner_terms_count'] = count( $keyword_terms ); } echo json_encode( $result ); exit; } static public function get_products() { $client_id = \S::get( 'client_id' ); $campaign_id = (int) \S::get( 'campaign_id' ); $ad_group_id = (int) \S::get( 'ad_group_id' ); $limit = \S::get( 'length' ) ? \S::get( 'length' ) : 10; $start = \S::get( 'start' ) ? \S::get( 'start' ) : 0; $order_dir = $_POST['order'][0]['dir'] ? strtoupper( $_POST['order'][0]['dir'] ) : 'DESC'; $order_name = $_POST['order'][0]['name'] ? $_POST['order'][0]['name'] : 'clicks'; $search = trim( (string) \S::get( 'search_text' ) ); $filter_cl4 = trim( (string) \S::get( 'filter_cl4' ) ); // ➊ MIN/MAX ROAS dla kontekstu klienta (opcjonalnie z filtrem search) $bounds = \factory\Products::get_roas_bounds( (int) $client_id, $search, $campaign_id, $ad_group_id, $filter_cl4 ); $roas_min = (float)$bounds['min']; $roas_max = (float)$bounds['max']; // zabezpieczenie przed dzieleniem przez 0 if ($roas_min === $roas_max) { $roas_max = $roas_min + 0.000001; } // ➋ Helper do paska performance (lokalna funkcja) $renderPerfBar = function (float $value, float $min, float $max): string { // normalizacja 0..1 $t = ($value - $min) / ($max - $min); if ($t < 0) $t = 0; if ($t > 1) $t = 1; // szerokości $minPx = 20; // minimalna długość paska $maxPx = 120; // szerokość „toru” $fill = (int)round($minPx + $t * ($maxPx - $minPx)); // kolor od #E74C3C (czerwony) do #2ECC71 (zielony) $from = [231, 76, 60]; $to = [ 46,204,113]; $r = (int)round($from[0] + ($to[0] - $from[0]) * $t); $g = (int)round($from[1] + ($to[1] - $from[1]) * $t); $b = (int)round($from[2] + ($to[2] - $from[2]) * $t); $hex = sprintf('#%02X%02X%02X', $r, $g, $b); // prosty pasek (tor + wypełnienie) return '
'; }; $db_results = \factory\Products::get_products( $client_id, $search, $limit, $start, $order_name, $order_dir, $campaign_id, $ad_group_id, $filter_cl4 ); $recordsTotal = \factory\Products::get_records_total_products( $client_id, $search, $campaign_id, $ad_group_id, $filter_cl4 ); // Sredni CR konta — do obliczenia progu klikniec $account_cr = \factory\Products::get_account_conversion_rate( (int) $client_id ); if ( $account_cr <= 0 ) $account_cr = 0.02; // fallback 2% $data['draw'] = \S::get( 'draw' ); $data['recordsTotal'] = $recordsTotal; $data['recordsFiltered'] = $recordsTotal; $data['data'] = []; foreach ( $db_results as $row ) { $custom_class = ''; $custom_label_4 = \factory\Products::get_product_data( $row['product_id'], 'custom_label_4' ); $custom_name = \factory\Products::get_product_data( $row['product_id'], 'title' ); $product_url = trim( (string) \factory\Products::get_product_data( $row['product_id'], 'product_url' ) ); if ( $custom_name ) { $row['name'] = $custom_name; $custom_class = 'custom_name'; } if ( $custom_label_4 == 'deleted' ) $custom_class = 'text-danger'; $custom_label_4_color = ''; if ( $custom_label_4 == 'bestseller' ) $custom_label_4_color = 'background-color:rgb(96, 119, 102); color: #FFF;'; else if ( $custom_label_4 == 'deleted' ) $custom_label_4_color = 'background-color:rgb(255, 0, 0); color: #FFF;'; else if ( $custom_label_4 == 'zombie' ) $custom_label_4_color = 'background-color:rgb(58, 58, 58); color: #FFF;'; else if ( $custom_label_4 == 'pla_single' ) $custom_label_4_color = 'background-color:rgb(49, 184, 9); color: #FFF;'; else if ( $custom_label_4 == 'pla' ) $custom_label_4_color = 'background-color:rgb(74, 63, 136); color: #FFF;'; else if ( $custom_label_4 == 'paused' ) $custom_label_4_color = 'background-color:rgb(143, 143, 143); color: #FFF;'; // ➌ ROAS – liczba + pasek performance $roasValue = (float)$row['roas']; $roasDisplay = (int) round( $roasValue, 0 ); $roasNumeric = ($roasValue <= (float)$row['min_roas']) ? '' . $roasDisplay . '' : $roasDisplay; $roasPerfBar = $renderPerfBar($roasValue, $roas_min, $roas_max); $roasCellHtml = '