Files
adsPRO/autoload/controls/class.Products.php
Jacek Pyziak b54a9a71b1 Add CLI script to fetch active Meta Ads insights for campaigns, adsets, and ads
- Implemented a new PHP script to retrieve insights for the last N days (default 30).
- Supports command-line options for token, account ID, days, API version, and output file.
- Fetches data at campaign, adset, and ad levels, with filtering for active statuses.
- Handles JSON output and optional file saving, including directory creation if necessary.
- Includes error handling for cURL requests and JSON responses.
2026-02-20 23:45:36 +01:00

1192 lines
42 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace controls;
class Products
{
static private function normalize_keyword_source_url( $url )
{
$url = trim( (string) $url );
if ( $url === '' )
{
return '';
}
$parts = parse_url( $url );
if ( !is_array( $parts ) || empty( $parts['scheme'] ) || empty( $parts['host'] ) )
{
return $url;
}
$normalized = strtolower( (string) $parts['scheme'] ) . '://' . strtolower( (string) $parts['host'] );
if ( isset( $parts['port'] ) )
{
$normalized .= ':' . (int) $parts['port'];
}
$normalized .= isset( $parts['path'] ) ? (string) $parts['path'] : '/';
if ( isset( $parts['query'] ) && $parts['query'] !== '' )
{
$normalized .= '?' . (string) $parts['query'];
}
return $normalized;
}
static public function sync_product_fields_to_merchant( $product_id, $changed_fields, $sync_source = 'products_ui' )
{
$product_id = (int) $product_id;
$changed_fields = is_array( $changed_fields ) ? $changed_fields : [];
$sync_source = trim( (string) $sync_source ) ?: 'products_ui';
if ( $product_id <= 0 || empty( $changed_fields ) )
{
return [ 'status' => 'skipped', 'message' => 'Brak zmian do synchronizacji.' ];
}
$supported_fields = [ 'title', 'description', 'google_product_category', 'custom_label_4' ];
$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_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_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 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 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' );
$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' ], 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 = $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 )
$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 = $_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 '<div class="roas-perf-wrap" title="Min: '.number_format($min,2).' | Max: '.number_format($max,2).'" style="margin-top:4px;width:'.$maxPx.'px;height:8px;background:#eee;border-radius:4px;overflow:hidden;">
<div class="roas-perf-fill" style="height:100%;width:'.$fill.'px;background:'.$hex.';"></div>
</div>';
};
$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' );
$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'])
? '<span class="text-danger text-bold">' . $roasDisplay . '</span>'
: $roasDisplay;
$roasPerfBar = $renderPerfBar($roasValue, $roas_min, $roas_max);
$roasCellHtml = '<div class="roas-cell">'.$roasNumeric.$roasPerfBar.'</div>';
$product_url_html = $product_url !== ''
? '<a href="' . htmlspecialchars( $product_url ) . '" target="_blank" rel="noopener noreferrer" title="' . htmlspecialchars( $product_url ) . '"><i class="fa-solid fa-up-right-from-square"></i> Otworz</a>'
: '';
$data['data'][] = [
'', // checkbox column
$row['product_id'],
$row['offer_id'],
htmlspecialchars( (string) ( $row['campaign_name'] ?? '' ) ),
htmlspecialchars( (string) ( $row['ad_group_name'] ?? '' ) ),
$product_url_html,
'<div class="table-product-title" product_id="' . $row['product_id'] . '">
<a href="/products/product_history/client_id=' . $client_id . '&product_id=' . $row['product_id'] . '&campaign_id=' . (int) ( $row['campaign_id'] ?? 0 ) . '&ad_group_id=' . (int) ( $row['ad_group_id'] ?? 0 ) . '" target="_blank" class="' . $custom_class . '">
' . $row['name'] . '
</a>
<span class="edit-product-title" product_id="' . $row['product_id'] . '">
<i class="fa fa-pencil"></i>
</span>
</div>',
$row['impressions'],
$row['impressions_30'],
'<span style="color: ' . ( $row['clicks'] > 200 ? ( $row['clicks'] > 400 ? '#0047ccff' : '#57b951' ) : '' ) . '">' . $row['clicks'] . '</span>',
$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,
'<input type="text" class="form-control min_roas" product_id="' . $row['product_id'] . '" value="' . $row['min_roas'] . '" style="width: 100px;">',
'',
'<input type="text" class="form-control custom_label_4" product_id="' . $row['product_id'] . '" value="' . $custom_label_4 . '" style="' . $custom_label_4_color . '">',
'<div class="btn-group btn-group-sm products-row-actions" role="group">'
. '<button type="button" class="btn btn-primary assign-product-scope" product_id="' . $row['product_id'] . '" title="Dodaj produkt do kampanii/grupy"><i class="fa-solid fa-diagram-project"></i></button>'
. '<button type="button" class="btn btn-secondary view-merchant-logs" product_id="' . $row['product_id'] . '" title="Pokaż logi synchronizacji Merchant"><i class="fa-solid fa-clock-rotate-left"></i></button>'
. '<button type="button" class="btn btn-danger delete-product" product_id="' . $row['product_id'] . '" title="Usuń produkt"><i class="fa-solid fa-trash"></i></button>'
. '</div>'
];
}
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' );
$old_custom_label_4 = (string) \factory\Products::get_product_data( $product_id, 'custom_label_4' );
if ( \factory\Products::set_product_data( $product_id, 'custom_label_4', $custom_label_4 ) )
{
self::sync_product_fields_to_merchant( $product_id, [
'custom_label_4' => [
'old' => $old_custom_label_4,
'new' => (string) $custom_label_4
]
], 'products_ui' );
\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 = '<div class="comment-cell">
<span class="comment-text">' . htmlspecialchars( $comment_data['comment'] ) . '</span>
<a href="#" class="delete-comment" data-comment-id="' . $comment_data['id'] . '">Usuń</a>
</div>';
}
$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 ?: '' );
if ( !empty( $changed_for_merchant ) )
{
self::sync_product_fields_to_merchant( $product_id, $changed_for_merchant, 'products_ui' );
}
}
\factory\Products::add_product_comment( $product_id, 'Zmiana tytułu i opisu produktu.' );
echo json_encode( [ 'status' => 'ok' ] );
exit;
}
}