'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 = '
'.$roasNumeric.$roasPerfBar.'
'; $data['data'][] = [ '', // checkbox column $row['product_id'], $row['offer_id'], htmlspecialchars( (string) ( $row['campaign_name'] ?? '' ) ), htmlspecialchars( (string) ( $row['ad_group_name'] ?? '' ) ), '
' . $row['name'] . '
', $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 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' ); if ( $product_id ) { if ( $custom_title ) \factory\Products::set_product_data( $product_id, 'title', $custom_title ); if ( $custom_description ) \factory\Products::set_product_data( $product_id, 'description', $custom_description ); if ( $google_product_category ) \factory\Products::set_product_data( $product_id, 'google_product_category', $google_product_category ); \factory\Products::set_product_data( $product_id, 'product_url', $product_url ?: '' ); } \factory\Products::add_product_comment( $product_id, 'Zmiana tytułu i opisu produktu.' ); echo json_encode( [ 'status' => 'ok' ] ); exit; } }