1368 lines
48 KiB
PHP
1368 lines
48 KiB
PHP
<?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' ];
|
||
$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' )
|
||
];
|
||
|
||
$stats = \factory\Products::get_client_bestseller_preview_stats( $client_id, $settings );
|
||
|
||
echo json_encode( [
|
||
'status' => 'ok',
|
||
'count' => (int) ( $stats['total_count'] ?? 0 ),
|
||
'entry_count' => (int) ( $stats['entry_count'] ?? 0 ),
|
||
'cooldown_count' => (int) ( $stats['cooldown_count'] ?? 0 ),
|
||
'total_count' => (int) ( $stats['total_count'] ?? 0 )
|
||
] );
|
||
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 '<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, $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'])
|
||
? '<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>'
|
||
: '';
|
||
|
||
// 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 = '<span class="product-warning-icon" data-warnings="' . $warningTitle . '" title="Produkt ma problemy"><i class="fa-solid fa-triangle-exclamation"></i></span>';
|
||
}
|
||
|
||
$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>',
|
||
$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,
|
||
'<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 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 = '<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 ?: '' );
|
||
|
||
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;
|
||
}
|
||
}
|