'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 = '
'.$roasNumeric.$roasPerfBar.'
'; $product_url_html = $product_url !== '' ? ' Otworz' : ''; // Algorytm ostrzezen produktowych $warnings = []; $clicks = (int) $row['clicks']; $conversions = (float) $row['conversions']; $ctr = (float) $row['ctr']; $min_roas_val = (float) $row['min_roas']; $click_threshold = (int) ceil( 3 * ( 1 / $account_cr ) ); // 1. Niski CTR po 30+ klikniec if ( $clicks >= 30 && $ctr < 0.5 ) { $warnings[] = 'Niski CTR (' . round( $ctr, 2 ) . '%) po ' . $clicks . ' kliknieciach — prawdopodobny problem z tytulem, zdjeciem lub cena.'; } // 2. Zero konwersji po progu klikniec if ( $clicks >= $click_threshold && $conversions == 0 ) { $warnings[] = 'Brak konwersji po ' . $clicks . ' kliknieciach (prog: ' . $click_threshold . '). Produkt prawdopodobnie nie sprzedaje sie z reklam.'; } // 3. ROAS ponizej progu rentownosci if ( $clicks >= $click_threshold && $conversions > 0 && $min_roas_val > 0 && $roasValue < $min_roas_val ) { $warnings[] = 'ROAS ' . $roasDisplay . '% ponizej progu rentownosci (' . (int) $min_roas_val . '%). Rozważ optymalizacje lub wylaczenie.'; } // 4. Strefa zolta — ROAS bliski progu (80-100% min_roas) if ( $clicks >= $click_threshold && $conversions > 0 && $min_roas_val > 0 && $roasValue >= $min_roas_val && $roasValue < $min_roas_val * 1.5 ) { $warnings[] = 'ROAS ' . $roasDisplay . '% — blisko progu rentownosci (' . (int) $min_roas_val . '%). Daj jeszcze 50-100 klikniec lub zoptymalizuj listing.'; } $warningHtml = ''; if ( !empty( $warnings ) ) { $warningTitle = htmlspecialchars( implode( "\n", $warnings ), ENT_QUOTES ); $warningHtml = ''; } $data['data'][] = [ '', // checkbox column $row['product_id'], $row['offer_id'], htmlspecialchars( (string) ( $row['campaign_name'] ?? '' ) ), htmlspecialchars( (string) ( $row['ad_group_name'] ?? '' ) ), $product_url_html, '
' . $row['name'] . '
', $warningHtml, $row['impressions'], $row['impressions_30'], $row['clicks'], $row['clicks_30'], round( $row['ctr'], 2 ) . '%', \S::number_display( $row['cost'] ), \S::number_display( $row['cpc'] ), round( $row['conversions'], 2 ), \S::number_display( $row['conversions_value'] ), $roasCellHtml, '', '', '', '
' . '' . '' . '' . '
' ]; } echo json_encode( $data ); exit; } static public function delete_product() { $product_id = \S::get( 'product_id' ); if ( \factory\Products::delete_product( $product_id ) ) echo json_encode( [ 'status' => 'ok' ] ); else echo json_encode( [ 'status' => 'error' ] ); exit; } static public function delete_products() { $product_ids = \S::get( 'product_ids' ); if ( !is_array( $product_ids ) || empty( $product_ids ) ) { echo json_encode( [ 'status' => 'error', 'message' => 'Brak produktów do usunięcia' ] ); exit; } if ( \factory\Products::delete_products( $product_ids ) ) echo json_encode( [ 'status' => 'ok' ] ); else echo json_encode( [ 'status' => 'error', 'message' => 'Błąd podczas usuwania produktów' ] ); exit; } static public function save_min_roas() { $product_id = \S::get( 'product_id' ); $min_roas = \S::get( 'min_roas' ); if ( \factory\Products::save_min_roas( $product_id, $min_roas ) ) { echo json_encode( [ 'status' => 'ok' ] ); } else echo json_encode( [ 'status' => 'error' ] ); exit; } static public function get_distinct_cl4() { $client_id = (int) \S::get( 'client_id' ); $values = \factory\Products::get_distinct_custom_label_4( $client_id ); echo json_encode( [ 'status' => 'ok', 'values' => $values ] ); exit; } static public function save_custom_label_4() { $product_id = \S::get( 'product_id' ); $custom_label_4 = \S::get( 'custom_label_4' ); if ( \factory\Products::set_product_data( $product_id, 'custom_label_4', $custom_label_4 ) ) { \factory\Products::add_product_comment( $product_id, 'Zmiana etykiety 4 na: ' . $custom_label_4 ); echo json_encode( [ 'status' => 'ok' ] ); } else echo json_encode( [ 'status' => 'error' ] ); exit; } static public function product_history() { $client_id = \S::get( 'client_id' ); $product_id = \S::get( 'product_id' ); $campaign_id = (int) \S::get( 'campaign_id' ); $ad_group_id = (int) \S::get( 'ad_group_id' ); return \Tpl::view( 'products/product_history', [ 'client_id' => $client_id, 'product_id' => $product_id, 'campaign_id' => $campaign_id, 'ad_group_id' => $ad_group_id, 'min_roas' => \factory\Products::get_min_roas( $product_id ) ] ); } static public function get_product_history_table() { $client_id= \S::get( 'client_id' ); $product_id = \S::get( 'product_id' ); $campaign_id = (int) \S::get( 'campaign_id' ); $ad_group_id = (int) \S::get( 'ad_group_id' ); $start = \S::get( 'start' ) ? \S::get( 'start' ) : 0; $limit = \S::get( 'length' ) ? \S::get( 'length' ) : 10; $db_results = \factory\Products::get_product_history( $client_id, $product_id, $start, $limit, $campaign_id, $ad_group_id ); $recordsTotal = \factory\Products::get_records_total_product_history( $client_id, $product_id, $campaign_id, $ad_group_id ); $data['draw'] = \S::get( 'draw' ); $data['recordsTotal'] = $recordsTotal; $data['recordsFiltered'] = $recordsTotal; $data['data'] = []; foreach ( $db_results as $row ) { $roas_value = ( $row['cost'] > 0) ? ( $row['conversions_value'] / $row['cost'] ) * 100 : 0; $roas = number_format( $roas_value, 0, '.', '' ) . '%'; $comment_data = \factory\Products::get_product_comment_by_date( $product_id, $row['date_add'] ); $comment_html = ''; if ( $comment_data ) { $comment_html = '
' . htmlspecialchars( $comment_data['comment'] ) . ' Usuń
'; } $data['data'][] = [ $row['id'], $row['impressions'], $row['clicks'], round( $row['ctr'], 2 ) . '%', \S::number_display( $row['cost'] ), $row['conversions'], \S::number_display( $row['conversions_value'] ), $roas, $row['date_add'], $comment_html, ]; } echo json_encode( $data ); exit; } static public function get_product_history_table_chart() { $client_id = \S::get( 'client_id' ); $product_id = \S::get( 'product_id' ); $campaign_id = (int) \S::get( 'campaign_id' ); $ad_group_id = (int) \S::get( 'ad_group_id' ); $limit = \S::get( 'length' ) ? \S::get( 'length' ) : 360; $start = \S::get( 'start' ) ? \S::get( 'start' ) : 0; $db_results = \factory\Products::get_product_history_30( $client_id, $product_id, $start, $limit, $campaign_id, $ad_group_id ); $impressions = []; $clicks = []; $ctr = []; $cost = []; $conversions = []; $conversions_value = []; $roas = []; $dates = []; foreach ( $db_results as $row ) { $impressions[] = (int)$row['impressions']; $clicks[] = (int)$row['clicks']; $ctr[] = (float)$row['ctr']; $cost[] = (float)$row['cost']; $conversions[] = (int)$row['conversions']; $conversions_value[] = (float)$row['conversions_value']; $roas[] = (float)$row['roas_all_time']; $dates[] = $row['date_add']; } $chart_data = [ [ 'name' => 'Wyświetlenia', 'data' => $impressions, 'visible' => false ], [ 'name' => 'Kliknięcia', 'data' => $clicks, 'visible' => false ], [ 'name' => 'CTR', 'data' => $ctr, 'visible' => false ], [ 'name' => 'Koszt', 'data' => $cost, 'visible' => false ], [ 'name' => 'Konwersje', 'data' => $conversions, 'visible' => false ], [ 'name' => 'Wartość konwersji', 'data' => $conversions_value, 'visible' => false ], [ 'name' => 'ROAS', 'data' => $roas ] ]; echo json_encode([ 'chart_data' => $chart_data, 'dates' => $dates, 'comments' => \factory\Products::get_product_comments( $product_id ), ]); exit; } static public function save_product_data() { $product_id = \S::get( 'product_id' ); $custom_title = \S::get( 'custom_title' ); $custom_description = \S::get( 'custom_description' ); $google_product_category = \S::get( 'google_product_category' ); $product_url = \S::get( 'product_url' ); $old_title = (string) \factory\Products::get_product_data( $product_id, 'title' ); $old_description = (string) \factory\Products::get_product_data( $product_id, 'description' ); $old_category = (string) \factory\Products::get_product_data( $product_id, 'google_product_category' ); $changed_for_merchant = []; if ( $product_id ) { if ( $custom_title ) { \factory\Products::set_product_data( $product_id, 'title', $custom_title ); $changed_for_merchant['title'] = [ 'old' => $old_title, 'new' => (string) $custom_title ]; } if ( $custom_description ) { \factory\Products::set_product_data( $product_id, 'description', $custom_description ); $changed_for_merchant['description'] = [ 'old' => $old_description, 'new' => (string) $custom_description ]; } if ( $google_product_category ) { \factory\Products::set_product_data( $product_id, 'google_product_category', $google_product_category ); $changed_for_merchant['google_product_category'] = [ 'old' => $old_category, 'new' => (string) $google_product_category ]; } \factory\Products::set_product_data( $product_id, 'product_url', $product_url ?: '' ); foreach ( $changed_for_merchant as $field => $change ) { if ( trim( $change['old'] ) === trim( $change['new'] ) ) { continue; } $log_old = $change['old']; $log_new = $change['new']; if ( $field === 'description' ) { $log_old = $log_old !== '' ? '(zmieniono)' : ''; $log_new = '(zmieniono)'; } \factory\Products::add_product_merchant_sync_log( [ 'product_id' => $product_id, 'field_name' => $field, 'old_value' => $log_old, 'new_value' => $log_new, 'sync_status' => 'local', 'sync_source' => 'products_ui' ] ); } } \factory\Products::add_product_comment( $product_id, 'Zmiana tytułu i opisu produktu.' ); echo json_encode( [ 'status' => 'ok' ] ); exit; } }