'ok', 'min_roas' => $min_roas ] ); } else echo json_encode( [ 'status' => 'error' ] ); exit; } static public function save_client_bestseller_min_roas() { $client_id = \S::get( 'client_id' ); $min_roas = \S::get( 'min_roas' ); if ( \factory\Products::save_client_bestseller_min_roas( $client_id, $min_roas ) ) { echo json_encode( [ 'status' => 'ok' ] ); } else echo json_encode( [ 'status' => 'error' ] ); exit; } 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' ); echo json_encode( [ 'campaigns' => \factory\Campaigns::get_campaigns_list( $client_id, true ) ] ); 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 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_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' => [ 'title' => $product_title, 'description' => $product_description, 'google_product_category' => $google_product_category, 'product_url' => $product_url ] ] ); 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 ( \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' ); $page_content = ''; if ( $product_url && filter_var( $product_url, FILTER_VALIDATE_URL ) ) { $page_content = \services\OpenAiApi::fetch_page_content( $product_url ); } $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, ]; $api = $provider === 'claude' ? \services\ClaudeApi::class : \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 ) $result['warning'] = '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; 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 = $_POST['search']['value']; // ➊ 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 ); $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 ); $recordsTotal = \factory\Products::get_records_total_products( $client_id, $search, $campaign_id, $ad_group_id ); $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' ); 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']; $roasNumeric = ($roasValue <= (float)$row['min_roas']) ? ''.($roasValue).'' : $roasValue; $roasPerfBar = $renderPerfBar($roasValue, $roas_min, $roas_max); $roasCellHtml = '