feat: Add XML file management functionality
- Created XmlFiles control class for handling XML file views and regeneration. - Implemented method to retrieve clients with XML feeds in the factory class. - Added database migration to include google_merchant_account_id in clients table. - Created migrations for products_keyword_planner_terms and products_merchant_sync_log tables. - Added campaign_keywords table migration for managing campaign keyword data. - Developed main view template for displaying XML files and their statuses. - Introduced a debug script for analyzing product URLs and their statuses.
This commit is contained in:
@@ -3,6 +3,72 @@ namespace controls;
|
||||
|
||||
class CampaignTerms
|
||||
{
|
||||
static private function is_google_ads_debug_enabled()
|
||||
{
|
||||
return \services\GoogleAdsApi::get_setting( 'google_ads_debug_enabled' ) !== '0';
|
||||
}
|
||||
|
||||
static private function with_optional_debug( $payload, $debug_data )
|
||||
{
|
||||
if ( self::is_google_ads_debug_enabled() )
|
||||
{
|
||||
$payload['debug'] = $debug_data;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
static private function normalize_ai_recommendations( $raw_json, $rows_by_id )
|
||||
{
|
||||
$decoded = json_decode( (string) $raw_json, true );
|
||||
$items = is_array( $decoded ) && isset( $decoded['items'] ) && is_array( $decoded['items'] ) ? $decoded['items'] : [];
|
||||
$recommendations = [];
|
||||
|
||||
foreach ( $items as $item )
|
||||
{
|
||||
$id = (int) ( $item['id'] ?? 0 );
|
||||
if ( $id <= 0 || !isset( $rows_by_id[$id] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$action_raw = strtolower( trim( (string) ( $item['action'] ?? '' ) ) );
|
||||
$action = $action_raw === 'exclude' ? 'exclude' : 'keep';
|
||||
$reason = trim( (string) ( $item['reason'] ?? '' ) );
|
||||
if ( $reason === '' )
|
||||
{
|
||||
$reason = $action === 'exclude' ? 'Niska trafnosc lub slabe wyniki.' : 'Fraza zostaje bez zmian.';
|
||||
}
|
||||
|
||||
if ( function_exists( 'mb_substr' ) )
|
||||
{
|
||||
$reason = mb_substr( $reason, 0, 120 );
|
||||
}
|
||||
else
|
||||
{
|
||||
$reason = substr( $reason, 0, 120 );
|
||||
}
|
||||
|
||||
$row = $rows_by_id[$id];
|
||||
|
||||
$recommendations[] = [
|
||||
'id' => $id,
|
||||
'search_term_id' => $id,
|
||||
'phrase' => trim( (string) ( $row['search_term'] ?? ( $item['phrase'] ?? '' ) ) ),
|
||||
'action' => $action,
|
||||
'reason' => $reason,
|
||||
'ad_group_name' => (string) ( $row['ad_group_name'] ?? '' ),
|
||||
'clicks_all_time' => (float) ( $row['clicks_all_time'] ?? 0 ),
|
||||
'cost_all_time' => (float) ( $row['cost_all_time'] ?? 0 ),
|
||||
'conversions_all_time' => (float) ( $row['conversions_all_time'] ?? 0 ),
|
||||
'conversion_value_all_time' => (float) ( $row['conversion_value_all_time'] ?? 0 ),
|
||||
'roas_all_time' => (float) ( $row['roas_all_time'] ?? 0 )
|
||||
];
|
||||
}
|
||||
|
||||
return $recommendations;
|
||||
}
|
||||
|
||||
static public function main_view()
|
||||
{
|
||||
return \Tpl::view( 'campaign_terms/main_view', [
|
||||
@@ -38,13 +104,14 @@ class CampaignTerms
|
||||
|
||||
if ( $campaign_id <= 0 )
|
||||
{
|
||||
echo json_encode( [ 'search_terms' => [], 'negative_keywords' => [] ] );
|
||||
echo json_encode( [ 'search_terms' => [], 'negative_keywords' => [], 'keywords' => [] ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode( [
|
||||
'search_terms' => \factory\Campaigns::get_campaign_search_terms( $campaign_id, $ad_group_id ),
|
||||
'negative_keywords' => \factory\Campaigns::get_campaign_negative_keywords( $campaign_id, $ad_group_id )
|
||||
'negative_keywords' => \factory\Campaigns::get_campaign_negative_keywords( $campaign_id, $ad_group_id ),
|
||||
'keywords' => \factory\Campaigns::get_campaign_keywords( $campaign_id, $ad_group_id )
|
||||
] );
|
||||
exit;
|
||||
}
|
||||
@@ -58,7 +125,15 @@ class CampaignTerms
|
||||
|
||||
if ( $search_term_id <= 0 )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Nie podano frazy do wykluczenia.' ] );
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Nie podano frazy do wykluczenia.',
|
||||
], [
|
||||
'search_term_id' => $search_term_id,
|
||||
'match_type_raw' => (string) \S::get( 'match_type' ),
|
||||
'scope_raw' => (string) \S::get( 'scope' ),
|
||||
'manual_keyword_text' => $manual_keyword_text
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -74,7 +149,15 @@ class CampaignTerms
|
||||
$context = \factory\Campaigns::get_search_term_context( $search_term_id );
|
||||
if ( !$context )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Nie znaleziono danych frazy.' ] );
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Nie znaleziono danych frazy.',
|
||||
], [
|
||||
'search_term_id' => $search_term_id,
|
||||
'manual_keyword_text' => $manual_keyword_text,
|
||||
'scope' => $scope,
|
||||
'match_type' => $match_type
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -97,26 +180,36 @@ class CampaignTerms
|
||||
|
||||
if ( $missing_data )
|
||||
{
|
||||
echo json_encode( [
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Brak wymaganych danych Google Ads dla tej frazy.',
|
||||
'debug' => [
|
||||
'customer_id' => $customer_id,
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'keyword_source' => $keyword_source,
|
||||
'scope' => $scope,
|
||||
'context' => $context
|
||||
]
|
||||
] );
|
||||
], [
|
||||
'customer_id' => $customer_id,
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'keyword_source' => $keyword_source,
|
||||
'scope' => $scope,
|
||||
'context' => $context
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
$api = new \services\GoogleAdsApi();
|
||||
if ( !$api -> is_configured() )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Google Ads API nie jest skonfigurowane.' ] );
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Google Ads API nie jest skonfigurowane.',
|
||||
], [
|
||||
'search_term_id' => $search_term_id,
|
||||
'customer_id' => $customer_id,
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'match_type' => $match_type,
|
||||
'scope' => $scope
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -132,21 +225,46 @@ class CampaignTerms
|
||||
if ( !( $api_result['success'] ?? false ) )
|
||||
{
|
||||
$last_error = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
|
||||
echo json_encode( [
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Nie udalo sie zapisac frazy wykluczajacej w Google Ads.',
|
||||
'error' => $last_error,
|
||||
'debug' => [
|
||||
'customer_id' => $customer_id,
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'keyword_source' => $keyword_source,
|
||||
'match_type' => $match_type,
|
||||
'scope' => $scope,
|
||||
'api_result' => $api_result
|
||||
]
|
||||
] );
|
||||
'error' => $last_error
|
||||
], [
|
||||
'customer_id' => $customer_id,
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'keyword_source' => $keyword_source,
|
||||
'match_type' => $match_type,
|
||||
'scope' => $scope,
|
||||
'api_result' => $api_result
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
$verification = $api_result['verification'] ?? null;
|
||||
$verification_found = true;
|
||||
if ( is_array( $verification ) && array_key_exists( 'found', $verification ) )
|
||||
{
|
||||
$verification_found = (bool) $verification['found'];
|
||||
}
|
||||
|
||||
if ( !$verification_found && !( $api_result['duplicate'] ?? false ) )
|
||||
{
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Google Ads API nie potwierdzilo dodania frazy po operacji create.',
|
||||
], [
|
||||
'customer_id' => $customer_id,
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'keyword_source' => $keyword_source,
|
||||
'match_type' => $match_type,
|
||||
'scope' => $scope,
|
||||
'api_result' => $api_result,
|
||||
'verification' => $verification
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -160,24 +278,395 @@ class CampaignTerms
|
||||
|
||||
$scope_label = $scope === 'campaign' ? 'kampanii' : 'grupy reklam';
|
||||
|
||||
echo json_encode( [
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => true,
|
||||
'message' => ( $api_result['duplicate'] ?? false ) ? 'Fraza byla juz wykluczona na poziomie ' . $scope_label . '.' : 'Fraza zostala dodana do wykluczajacych na poziomie ' . $scope_label . '.',
|
||||
'duplicate' => (bool) ( $api_result['duplicate'] ?? false ),
|
||||
'match_type' => $match_type,
|
||||
'scope' => $scope
|
||||
], [
|
||||
'customer_id' => $customer_id,
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'keyword_source' => $keyword_source,
|
||||
'scope' => $scope,
|
||||
'debug' => [
|
||||
'customer_id' => $customer_id,
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'keyword_source' => $keyword_source,
|
||||
'scope' => $scope,
|
||||
'api_response' => $api_result['response'] ?? null,
|
||||
'sent_operation' => $api_result['sent_operation'] ?? null,
|
||||
'verification' => $api_result['verification'] ?? null
|
||||
]
|
||||
'api_response' => $api_result['response'] ?? null,
|
||||
'sent_operation' => $api_result['sent_operation'] ?? null,
|
||||
'verification' => $api_result['verification'] ?? null
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function analyze_search_terms_with_ai()
|
||||
{
|
||||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||||
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||||
$search_term_ids_raw = \S::get( 'search_term_ids' );
|
||||
|
||||
if ( $campaign_id <= 0 )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Wybierz kampanie.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( \services\GoogleAdsApi::get_setting( 'openai_enabled' ) === '0' )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'OpenAI jest wylaczone. Wlacz je w Ustawieniach.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( !\services\OpenAiApi::is_configured() )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Klucz API OpenAI nie jest skonfigurowany. Przejdz do Ustawien.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$rows = \factory\Campaigns::get_campaign_search_terms( $campaign_id, $ad_group_id );
|
||||
|
||||
$ids_filter = [];
|
||||
if ( is_array( $search_term_ids_raw ) )
|
||||
{
|
||||
foreach ( $search_term_ids_raw as $id_raw )
|
||||
{
|
||||
$id = (int) $id_raw;
|
||||
if ( $id > 0 )
|
||||
{
|
||||
$ids_filter[$id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif ( $search_term_ids_raw !== null && $search_term_ids_raw !== '' )
|
||||
{
|
||||
$id = (int) $search_term_ids_raw;
|
||||
if ( $id > 0 )
|
||||
{
|
||||
$ids_filter[$id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !empty( $ids_filter ) )
|
||||
{
|
||||
$rows = array_values( array_filter( $rows, function( $row ) use ( $ids_filter )
|
||||
{
|
||||
$id = (int) ( $row['id'] ?? 0 );
|
||||
return $id > 0 && isset( $ids_filter[$id] );
|
||||
} ) );
|
||||
}
|
||||
|
||||
if ( empty( $rows ) )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Brak fraz do analizy.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$rows = array_slice( $rows, 0, 150 );
|
||||
|
||||
$rows_by_id = [];
|
||||
foreach ( $rows as $row )
|
||||
{
|
||||
$id = (int) ( $row['id'] ?? 0 );
|
||||
if ( $id > 0 )
|
||||
{
|
||||
$rows_by_id[$id] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$campaign_name = '';
|
||||
$campaign_type = '';
|
||||
if ( !empty( $rows ) )
|
||||
{
|
||||
$campaign_name = trim( (string) ( $rows[0]['campaign_name'] ?? '' ) );
|
||||
$campaign_type = trim( (string) ( $rows[0]['advertising_channel_type'] ?? '' ) );
|
||||
}
|
||||
|
||||
$ad_group_name = '';
|
||||
if ( $ad_group_id > 0 && !empty( $rows ) )
|
||||
{
|
||||
$ad_group_name = trim( (string) ( $rows[0]['ad_group_name'] ?? '' ) );
|
||||
}
|
||||
|
||||
$result = \services\OpenAiApi::suggest_negative_keywords_to_exclude( $rows, [
|
||||
'campaign_name' => $campaign_name,
|
||||
'campaign_type' => $campaign_type,
|
||||
'ad_group_name' => $ad_group_name,
|
||||
'ad_group_id' => $ad_group_id
|
||||
] );
|
||||
|
||||
if ( ( $result['status'] ?? 'error' ) !== 'ok' )
|
||||
{
|
||||
echo json_encode( [
|
||||
'success' => false,
|
||||
'message' => (string) ( $result['message'] ?? 'Blad analizy OpenAI.' )
|
||||
] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$raw_json = (string) ( $result['suggestion'] ?? '' );
|
||||
$recommendations = self::normalize_ai_recommendations( $raw_json, $rows_by_id );
|
||||
$exclude_count = count( array_filter( $recommendations, function( $item )
|
||||
{
|
||||
return ( $item['action'] ?? '' ) === 'exclude';
|
||||
} ) );
|
||||
|
||||
if ( empty( $recommendations ) )
|
||||
{
|
||||
echo json_encode( [
|
||||
'success' => false,
|
||||
'message' => 'Nie udalo sie sparsowac odpowiedzi AI.',
|
||||
'raw' => $raw_json
|
||||
] );
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode( [
|
||||
'success' => true,
|
||||
'message' => 'Analiza zakonczona. Proponowane wykluczenia: ' . $exclude_count . '.',
|
||||
'analyzed_count' => count( $rows ),
|
||||
'exclude_count' => $exclude_count,
|
||||
'recommendations' => $recommendations
|
||||
] );
|
||||
exit;
|
||||
}
|
||||
|
||||
static private function delete_negative_keyword_row( $negative_keyword_id )
|
||||
{
|
||||
$negative_keyword_id = (int) $negative_keyword_id;
|
||||
|
||||
if ( $negative_keyword_id <= 0 )
|
||||
{
|
||||
return [ 'success' => false, 'message' => 'Nie podano frazy do usuniecia.' ];
|
||||
}
|
||||
|
||||
$context = \factory\Campaigns::get_negative_keyword_context( $negative_keyword_id );
|
||||
if ( !$context )
|
||||
{
|
||||
return [ 'success' => false, 'message' => 'Nie znaleziono danych frazy wykluczajacej.' ];
|
||||
}
|
||||
|
||||
$customer_id = trim( (string) ( $context['google_ads_customer_id'] ?? '' ) );
|
||||
$scope = strtolower( trim( (string) ( $context['scope'] ?? 'campaign' ) ) );
|
||||
$match_type = strtoupper( trim( (string) ( $context['match_type'] ?? 'PHRASE' ) ) );
|
||||
$keyword_text = trim( (string) ( $context['keyword_text'] ?? '' ) );
|
||||
$campaign_external_id = trim( (string) ( $context['external_campaign_id'] ?? '' ) );
|
||||
$ad_group_external_id = trim( (string) ( $context['external_ad_group_id'] ?? '' ) );
|
||||
|
||||
if ( !in_array( $scope, [ 'campaign', 'ad_group' ], true ) )
|
||||
{
|
||||
$scope = 'campaign';
|
||||
}
|
||||
|
||||
$missing_data = ( $customer_id === '' || $keyword_text === '' );
|
||||
if ( $scope === 'campaign' && $campaign_external_id === '' )
|
||||
{
|
||||
$missing_data = true;
|
||||
}
|
||||
if ( $scope === 'ad_group' && $ad_group_external_id === '' )
|
||||
{
|
||||
$missing_data = true;
|
||||
}
|
||||
|
||||
if ( $missing_data )
|
||||
{
|
||||
return self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Brak wymaganych danych Google Ads dla tej frazy.'
|
||||
], [
|
||||
'context' => $context
|
||||
] );
|
||||
}
|
||||
|
||||
$api = new \services\GoogleAdsApi();
|
||||
if ( !$api -> is_configured() )
|
||||
{
|
||||
return self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Google Ads API nie jest skonfigurowane.'
|
||||
], [
|
||||
'customer_id' => $customer_id,
|
||||
'scope' => $scope,
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'match_type' => $match_type
|
||||
] );
|
||||
}
|
||||
|
||||
if ( $scope === 'campaign' )
|
||||
{
|
||||
$api_result = $api -> remove_negative_keyword_from_campaign( $customer_id, $campaign_external_id, $keyword_text, $match_type );
|
||||
}
|
||||
else
|
||||
{
|
||||
$api_result = $api -> remove_negative_keyword_from_ad_group( $customer_id, $ad_group_external_id, $keyword_text, $match_type );
|
||||
}
|
||||
|
||||
if ( !( $api_result['success'] ?? false ) )
|
||||
{
|
||||
$last_error = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
|
||||
return self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Nie udalo sie usunac frazy wykluczajacej w Google Ads.',
|
||||
'error' => $last_error
|
||||
], [
|
||||
'customer_id' => $customer_id,
|
||||
'scope' => $scope,
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'match_type' => $match_type,
|
||||
'api_result' => $api_result
|
||||
] );
|
||||
}
|
||||
|
||||
\factory\Campaigns::delete_campaign_negative_keyword( $negative_keyword_id );
|
||||
|
||||
$removed = (int) ( $api_result['removed'] ?? 0 );
|
||||
$scope_label = $scope === 'campaign' ? 'kampanii' : 'grupy reklam';
|
||||
$message = $removed > 0
|
||||
? 'Fraza zostala usunieta z wykluczajacych na poziomie ' . $scope_label . '.'
|
||||
: 'Fraza nie byla juz obecna w Google Ads. Usunieto lokalny wpis.';
|
||||
|
||||
return self::with_optional_debug( [
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'removed' => $removed,
|
||||
'negative_keyword_id' => $negative_keyword_id
|
||||
], [
|
||||
'customer_id' => $customer_id,
|
||||
'scope' => $scope,
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'match_type' => $match_type,
|
||||
'api_result' => $api_result
|
||||
] );
|
||||
}
|
||||
|
||||
static public function delete_negative_keyword()
|
||||
{
|
||||
$negative_keyword_id = (int) \S::get( 'negative_keyword_id' );
|
||||
$result = self::delete_negative_keyword_row( $negative_keyword_id );
|
||||
echo json_encode( $result );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function delete_negative_keywords()
|
||||
{
|
||||
$negative_keyword_ids_raw = \S::get( 'negative_keyword_ids' );
|
||||
|
||||
if ( !is_array( $negative_keyword_ids_raw ) )
|
||||
{
|
||||
$negative_keyword_ids_raw = [ $negative_keyword_ids_raw ];
|
||||
}
|
||||
|
||||
$negative_keyword_ids = [];
|
||||
foreach ( $negative_keyword_ids_raw as $id_raw )
|
||||
{
|
||||
$id = (int) $id_raw;
|
||||
if ( $id > 0 )
|
||||
{
|
||||
$negative_keyword_ids[] = $id;
|
||||
}
|
||||
}
|
||||
|
||||
$negative_keyword_ids = array_values( array_unique( $negative_keyword_ids ) );
|
||||
|
||||
if ( empty( $negative_keyword_ids ) )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Nie podano fraz do usuniecia.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$deleted_count = 0;
|
||||
$failed = [];
|
||||
$debug = [];
|
||||
$total_count = count( $negative_keyword_ids );
|
||||
|
||||
foreach ( $negative_keyword_ids as $negative_keyword_id )
|
||||
{
|
||||
$result = self::delete_negative_keyword_row( $negative_keyword_id );
|
||||
|
||||
if ( $result['success'] ?? false )
|
||||
{
|
||||
$deleted_count++;
|
||||
|
||||
$debug[] = [
|
||||
'id' => $negative_keyword_id,
|
||||
'success' => true,
|
||||
'message' => (string) ( $result['message'] ?? '' ),
|
||||
'debug' => $result['debug'] ?? null
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$failed[] = [
|
||||
'id' => $negative_keyword_id,
|
||||
'message' => (string) ( $result['message'] ?? 'Nieznany blad' ),
|
||||
'error' => (string) ( $result['error'] ?? '' ),
|
||||
'debug' => $result['debug'] ?? null
|
||||
];
|
||||
|
||||
$debug[] = [
|
||||
'id' => $negative_keyword_id,
|
||||
'success' => false,
|
||||
'message' => (string) ( $result['message'] ?? 'Nieznany blad' ),
|
||||
'error' => (string) ( $result['error'] ?? '' ),
|
||||
'debug' => $result['debug'] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
$failed_count = count( $failed );
|
||||
|
||||
if ( $deleted_count === $total_count )
|
||||
{
|
||||
$response = [
|
||||
'success' => true,
|
||||
'message' => 'Usunieto zaznaczone frazy wykluczajace (' . $deleted_count . ').',
|
||||
'deleted_count' => $deleted_count,
|
||||
'failed_count' => 0
|
||||
];
|
||||
if ( self::is_google_ads_debug_enabled() )
|
||||
{
|
||||
$response['debug'] = $debug;
|
||||
}
|
||||
|
||||
echo json_encode( $response );
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( $deleted_count > 0 )
|
||||
{
|
||||
$response = [
|
||||
'success' => true,
|
||||
'partial' => true,
|
||||
'message' => 'Usunieto ' . $deleted_count . ' z ' . $total_count . ' zaznaczonych fraz wykluczajacych.',
|
||||
'deleted_count' => $deleted_count,
|
||||
'failed_count' => $failed_count,
|
||||
'failed' => $failed
|
||||
];
|
||||
if ( self::is_google_ads_debug_enabled() )
|
||||
{
|
||||
$response['debug'] = $debug;
|
||||
}
|
||||
|
||||
echo json_encode( $response );
|
||||
exit;
|
||||
}
|
||||
|
||||
$response = [
|
||||
'success' => false,
|
||||
'message' => 'Nie udalo sie usunac zaznaczonych fraz wykluczajacych.',
|
||||
'deleted_count' => 0,
|
||||
'failed_count' => $failed_count,
|
||||
'failed' => $failed
|
||||
];
|
||||
if ( self::is_google_ads_debug_enabled() )
|
||||
{
|
||||
$response['debug'] = $debug;
|
||||
}
|
||||
|
||||
echo json_encode( $response );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class Clients
|
||||
$id = \S::get( 'id' );
|
||||
$name = trim( \S::get( 'name' ) );
|
||||
$google_ads_customer_id = trim( \S::get( 'google_ads_customer_id' ) );
|
||||
$google_merchant_account_id = trim( \S::get( 'google_merchant_account_id' ) );
|
||||
|
||||
if ( !$name )
|
||||
{
|
||||
@@ -28,6 +29,7 @@ class Clients
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'google_ads_customer_id' => $google_ads_customer_id ?: null,
|
||||
'google_merchant_account_id' => $google_merchant_account_id ?: null,
|
||||
'google_ads_start_date' => $google_ads_start_date ?: null,
|
||||
];
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,168 @@
|
||||
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 get_client_bestseller_min_roas() {
|
||||
$client_id = \S::get( 'client_id' );
|
||||
|
||||
@@ -57,6 +219,233 @@ class Products
|
||||
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' );
|
||||
@@ -102,6 +491,31 @@ class Products
|
||||
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' );
|
||||
@@ -144,12 +558,51 @@ class Products
|
||||
|
||||
// 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' ),
|
||||
@@ -165,6 +618,7 @@ class Products
|
||||
'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;
|
||||
@@ -187,10 +641,19 @@ class Products
|
||||
$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.';
|
||||
$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;
|
||||
}
|
||||
@@ -257,6 +720,7 @@ class Products
|
||||
$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 )
|
||||
{
|
||||
@@ -283,12 +747,16 @@ class Products
|
||||
|
||||
// ➌ 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">'.($roasValue).'</span>'
|
||||
: $roasValue;
|
||||
? '<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
|
||||
@@ -296,6 +764,7 @@ class Products
|
||||
$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'] . '
|
||||
@@ -317,7 +786,11 @@ class Products
|
||||
'<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 . '">',
|
||||
'<button type="button" class="btn btn-danger btn-sm delete-product" product_id="' . $row['product_id'] . '"><i class="fa-solid fa-trash"></i></button>'
|
||||
'<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>'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -368,9 +841,17 @@ class Products
|
||||
{
|
||||
$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' ] );
|
||||
}
|
||||
@@ -423,7 +904,7 @@ class Products
|
||||
{
|
||||
$comment_html = '<div class="comment-cell">
|
||||
<span class="comment-text">' . htmlspecialchars( $comment_data['comment'] ) . '</span>
|
||||
<a href="#" class="text-danger delete-comment" data-comment-id="' . $comment_data['id'] . '" style="margin-left: 10px;">Usuń</a>
|
||||
<a href="#" class="delete-comment" data-comment-id="' . $comment_data['id'] . '">Usuń</a>
|
||||
</div>';
|
||||
}
|
||||
|
||||
@@ -524,18 +1005,38 @@ class Products
|
||||
$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.' );
|
||||
|
||||
@@ -71,10 +71,33 @@ class Users
|
||||
}
|
||||
|
||||
return \view\Users::settings(
|
||||
$user
|
||||
$user,
|
||||
self::get_cron_dashboard_data()
|
||||
);
|
||||
}
|
||||
|
||||
public static function settings_cron_status()
|
||||
{
|
||||
global $user;
|
||||
|
||||
header( 'Content-Type: application/json; charset=utf-8' );
|
||||
header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
|
||||
header( 'Pragma: no-cache' );
|
||||
|
||||
if ( !$user )
|
||||
{
|
||||
http_response_code( 403 );
|
||||
echo json_encode( [ 'status' => 'error', 'message' => 'Brak autoryzacji.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode( [
|
||||
'status' => 'ok',
|
||||
'data' => self::get_cron_dashboard_data()
|
||||
], JSON_UNESCAPED_UNICODE );
|
||||
exit;
|
||||
}
|
||||
|
||||
public static function settings_save_google_ads()
|
||||
{
|
||||
$fields = [
|
||||
@@ -82,6 +105,7 @@ class Users
|
||||
'google_ads_client_id',
|
||||
'google_ads_client_secret',
|
||||
'google_ads_refresh_token',
|
||||
'google_merchant_refresh_token',
|
||||
'google_ads_manager_account_id',
|
||||
];
|
||||
|
||||
@@ -90,9 +114,13 @@ class Users
|
||||
\services\GoogleAdsApi::set_setting( $field, \S::get( $field ) );
|
||||
}
|
||||
|
||||
\services\GoogleAdsApi::set_setting( 'google_ads_debug_enabled', \S::get( 'google_ads_debug_enabled' ) ? '1' : '0' );
|
||||
|
||||
// wyczyść cached token przy zmianie credentials
|
||||
\services\GoogleAdsApi::set_setting( 'google_ads_access_token', null );
|
||||
\services\GoogleAdsApi::set_setting( 'google_ads_access_token_expires', null );
|
||||
\services\GoogleAdsApi::set_setting( 'google_merchant_access_token', null );
|
||||
\services\GoogleAdsApi::set_setting( 'google_merchant_access_token_expires', null );
|
||||
|
||||
\S::alert( 'Ustawienia Google Ads zostały zapisane.' );
|
||||
header( 'Location: /settings' );
|
||||
@@ -121,6 +149,293 @@ class Users
|
||||
exit;
|
||||
}
|
||||
|
||||
private static function get_cron_dashboard_data()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$base_url = self::get_base_url();
|
||||
$clients_total = (int) $mdb -> query( "SELECT COUNT(*) FROM clients WHERE deleted = 0 AND google_ads_customer_id IS NOT NULL AND google_ads_customer_id <> ''" ) -> fetchColumn();
|
||||
|
||||
$campaign_window_state = self::get_setting_json( 'cron_campaigns_window_state' );
|
||||
$campaign_daily_state = self::get_setting_json( 'cron_campaigns_state' );
|
||||
$campaign_dates = self::normalize_dates( $campaign_window_state['sync_dates'] ?? [] );
|
||||
$campaign_dates_count = count( $campaign_dates );
|
||||
if ( $campaign_dates_count < 1 )
|
||||
{
|
||||
$campaign_dates = [ date( 'Y-m-d' ) ];
|
||||
$campaign_dates_count = 1;
|
||||
}
|
||||
|
||||
$campaign_current_date_index = (int) ( $campaign_window_state['current_date_index'] ?? 0 );
|
||||
$campaign_current_date_index = max( 0, min( $campaign_dates_count - 1, $campaign_current_date_index ) );
|
||||
$campaign_processed_today = count( self::normalize_ids( $campaign_daily_state['processed_ids'] ?? [] ) );
|
||||
$campaign_processed_today = min( $clients_total, $campaign_processed_today );
|
||||
$campaign_total = $clients_total * $campaign_dates_count;
|
||||
$campaign_processed = min( $campaign_total, ( $campaign_current_date_index * $clients_total ) + $campaign_processed_today );
|
||||
$campaign_remaining = max( 0, $campaign_total - $campaign_processed );
|
||||
$campaign_active_date = $campaign_window_state['sync_date'] ?? ( $campaign_dates[ $campaign_current_date_index ] ?? '' );
|
||||
$campaign_meta = 'Aktywny dzień: ' . ( $campaign_active_date ?: '-' ) . ', okno dni: ' . $campaign_dates_count;
|
||||
$campaign_eta_meta = self::build_eta_meta( 'cron_campaigns', $campaign_remaining );
|
||||
if ( $campaign_eta_meta !== '' )
|
||||
{
|
||||
$campaign_meta .= ', ' . $campaign_eta_meta;
|
||||
}
|
||||
|
||||
$products_state = self::get_setting_json( 'cron_products_pipeline_state' );
|
||||
$products_dates = self::normalize_dates( $products_state['import_dates'] ?? [] );
|
||||
$products_dates_count = count( $products_dates );
|
||||
if ( $products_dates_count < 1 )
|
||||
{
|
||||
$products_dates = [ date( 'Y-m-d' ) ];
|
||||
$products_dates_count = 1;
|
||||
}
|
||||
|
||||
$products_current_date_index = (int) ( $products_state['current_date_index'] ?? 0 );
|
||||
$products_current_date_index = max( 0, min( $products_dates_count - 1, $products_current_date_index ) );
|
||||
$products_phase = (string) ( $products_state['phase'] ?? 'fetch' );
|
||||
|
||||
$products_fetch_done = count( self::normalize_ids( $products_state['fetch_done_ids'] ?? [] ) );
|
||||
$products_aggregate_30_done = count( self::normalize_ids( $products_state['aggregate_30_done_ids'] ?? [] ) );
|
||||
$products_aggregate_temp_done = count( self::normalize_ids( $products_state['aggregate_temp_done_ids'] ?? [] ) );
|
||||
|
||||
$products_fetch_done = min( $clients_total, $products_fetch_done );
|
||||
$products_aggregate_30_done = min( $clients_total, $products_aggregate_30_done );
|
||||
$products_aggregate_temp_done = min( $clients_total, $products_aggregate_temp_done );
|
||||
|
||||
$products_per_day_total = $clients_total * 3;
|
||||
$products_total = $products_per_day_total * $products_dates_count;
|
||||
|
||||
$products_done_in_day = 0;
|
||||
if ( $products_phase === 'aggregate_30' )
|
||||
{
|
||||
$products_done_in_day = $clients_total + $products_aggregate_30_done;
|
||||
}
|
||||
else if ( $products_phase === 'aggregate_temp' )
|
||||
{
|
||||
$products_done_in_day = ( $clients_total * 2 ) + $products_aggregate_temp_done;
|
||||
}
|
||||
else if ( $products_phase === 'done' )
|
||||
{
|
||||
$products_done_in_day = $products_per_day_total;
|
||||
}
|
||||
else
|
||||
{
|
||||
$products_done_in_day = $products_fetch_done;
|
||||
}
|
||||
|
||||
$products_done_in_day = min( $products_per_day_total, $products_done_in_day );
|
||||
$products_processed = min( $products_total, ( $products_current_date_index * $products_per_day_total ) + $products_done_in_day );
|
||||
if ( $products_phase === 'done' )
|
||||
{
|
||||
$products_processed = $products_total;
|
||||
}
|
||||
$products_remaining = max( 0, $products_total - $products_processed );
|
||||
|
||||
$products_phase_labels = [
|
||||
'fetch' => 'Pobieranie',
|
||||
'aggregate_30' => 'Agregacja 30 dni',
|
||||
'aggregate_temp' => 'Agregacja temp',
|
||||
'done' => 'Zakończono'
|
||||
];
|
||||
$products_phase_label = $products_phase_labels[ $products_phase ] ?? $products_phase;
|
||||
$products_active_date = $products_state['import_date'] ?? ( $products_dates[ $products_current_date_index ] ?? '' );
|
||||
$products_meta = 'Faza: ' . $products_phase_label . ', aktywny dzień: ' . ( $products_active_date ?: '-' ) . ', okno dni: ' . $products_dates_count;
|
||||
$products_eta_meta = self::build_eta_meta( 'cron_products', $products_remaining );
|
||||
if ( $products_eta_meta !== '' )
|
||||
{
|
||||
$products_meta .= ', ' . $products_eta_meta;
|
||||
}
|
||||
|
||||
$cron_endpoints = [
|
||||
[ 'name' => 'Legacy CRON', 'path' => '/cron.php', 'action' => 'cron_legacy' ],
|
||||
[ 'name' => 'Cron kampanii', 'path' => '/cron/cron_campaigns', 'action' => 'cron_campaigns' ],
|
||||
[ 'name' => 'Cron produktów', 'path' => '/cron/cron_products', 'action' => 'cron_products' ],
|
||||
[ 'name' => 'Cron URL produktów (Merchant)', 'path' => '/cron/cron_products_urls', 'action' => 'cron_products_urls' ],
|
||||
[ 'name' => 'Cron fraz', 'path' => '/cron/cron_phrases', 'action' => 'cron_phrases' ],
|
||||
[ 'name' => 'Historia 30 dni produktów', 'path' => '/cron/cron_products_history_30', 'action' => 'cron_products_history_30' ],
|
||||
[ 'name' => 'Historia 30 dni fraz', 'path' => '/cron/cron_phrases_history_30', 'action' => 'cron_phrases_history_30' ],
|
||||
[ 'name' => 'Eksport XML', 'path' => '/cron/cron_xml', 'action' => 'cron_xml' ],
|
||||
];
|
||||
|
||||
$urls = [];
|
||||
foreach ( $cron_endpoints as $endpoint )
|
||||
{
|
||||
$last_key = 'cron_last_invoked_' . $endpoint['action'] . '_at';
|
||||
$urls[] = [
|
||||
'name' => $endpoint['name'],
|
||||
'url' => $base_url . $endpoint['path'],
|
||||
'last_invoked_at' => self::format_datetime( \services\GoogleAdsApi::get_setting( $last_key ) ),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'overall_last_invoked_at' => self::format_datetime( \services\GoogleAdsApi::get_setting( 'cron_last_invoked_at' ) ),
|
||||
'clients_total' => $clients_total,
|
||||
'progress' => [
|
||||
[
|
||||
'name' => 'Kampanie',
|
||||
'processed' => $campaign_processed,
|
||||
'total' => $campaign_total,
|
||||
'percent' => self::progress_percent( $campaign_processed, $campaign_total ),
|
||||
'meta' => $campaign_meta
|
||||
],
|
||||
[
|
||||
'name' => 'Produkty',
|
||||
'processed' => $products_processed,
|
||||
'total' => $products_total,
|
||||
'percent' => self::progress_percent( $products_processed, $products_total ),
|
||||
'meta' => $products_meta
|
||||
],
|
||||
],
|
||||
'urls' => $urls
|
||||
];
|
||||
}
|
||||
|
||||
private static function get_setting_json( $setting_key )
|
||||
{
|
||||
$raw = \services\GoogleAdsApi::get_setting( $setting_key );
|
||||
if ( !$raw )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode( (string) $raw, true );
|
||||
return is_array( $decoded ) ? $decoded : [];
|
||||
}
|
||||
|
||||
private static function normalize_ids( $items )
|
||||
{
|
||||
$result = [];
|
||||
foreach ( (array) $items as $item )
|
||||
{
|
||||
$id = (int) $item;
|
||||
if ( $id > 0 )
|
||||
{
|
||||
$result[] = $id;
|
||||
}
|
||||
}
|
||||
return array_values( array_unique( $result ) );
|
||||
}
|
||||
|
||||
private static function normalize_dates( $items )
|
||||
{
|
||||
$result = [];
|
||||
foreach ( (array) $items as $item )
|
||||
{
|
||||
$timestamp = strtotime( (string) $item );
|
||||
if ( !$timestamp )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$result[] = date( 'Y-m-d', $timestamp );
|
||||
}
|
||||
$result = array_values( array_unique( $result ) );
|
||||
sort( $result );
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function progress_percent( $processed, $total )
|
||||
{
|
||||
$processed = (int) $processed;
|
||||
$total = (int) $total;
|
||||
|
||||
if ( $total <= 0 )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) round( min( 100, max( 0, ( $processed / $total ) * 100 ) ) );
|
||||
}
|
||||
|
||||
private static function build_eta_meta( $action_name, $remaining_tasks )
|
||||
{
|
||||
$remaining_tasks = max( 0, (int) $remaining_tasks );
|
||||
|
||||
if ( $remaining_tasks <= 0 )
|
||||
{
|
||||
return 'Szacowany koniec: zakończono';
|
||||
}
|
||||
|
||||
$avg_interval_seconds = (float) \services\GoogleAdsApi::get_setting( 'cron_avg_interval_' . $action_name . '_sec' );
|
||||
$last_interval_seconds = (int) \services\GoogleAdsApi::get_setting( 'cron_last_interval_' . $action_name . '_sec' );
|
||||
|
||||
if ( $avg_interval_seconds <= 0 && $last_interval_seconds > 0 )
|
||||
{
|
||||
$avg_interval_seconds = (float) $last_interval_seconds;
|
||||
}
|
||||
|
||||
if ( $avg_interval_seconds <= 0 )
|
||||
{
|
||||
return 'Szacowany koniec: brak danych o częstotliwości';
|
||||
}
|
||||
|
||||
$estimated_seconds = (int) max( 1, round( $remaining_tasks * $avg_interval_seconds ) );
|
||||
$eta_timestamp = time() + $estimated_seconds;
|
||||
|
||||
return 'Śr. interwał: '
|
||||
. self::format_duration_short( (int) round( $avg_interval_seconds ) )
|
||||
. ', szacowany koniec: '
|
||||
. date( 'Y-m-d H:i:s', $eta_timestamp )
|
||||
. ' (za '
|
||||
. self::format_duration_short( $estimated_seconds )
|
||||
. ')';
|
||||
}
|
||||
|
||||
private static function format_duration_short( $seconds )
|
||||
{
|
||||
$seconds = max( 0, (int) $seconds );
|
||||
|
||||
if ( $seconds < 60 )
|
||||
{
|
||||
return $seconds . ' sek';
|
||||
}
|
||||
|
||||
$days = (int) floor( $seconds / 86400 );
|
||||
$seconds -= $days * 86400;
|
||||
$hours = (int) floor( $seconds / 3600 );
|
||||
$seconds -= $hours * 3600;
|
||||
$minutes = (int) floor( $seconds / 60 );
|
||||
|
||||
$parts = [];
|
||||
if ( $days > 0 )
|
||||
{
|
||||
$parts[] = $days . ' d';
|
||||
}
|
||||
if ( $hours > 0 )
|
||||
{
|
||||
$parts[] = $hours . ' h';
|
||||
}
|
||||
if ( $minutes > 0 )
|
||||
{
|
||||
$parts[] = $minutes . ' min';
|
||||
}
|
||||
|
||||
if ( empty( $parts ) )
|
||||
{
|
||||
return '1 min';
|
||||
}
|
||||
|
||||
return implode( ' ', array_slice( $parts, 0, 2 ) );
|
||||
}
|
||||
|
||||
private static function format_datetime( $value )
|
||||
{
|
||||
$timestamp = strtotime( (string) $value );
|
||||
if ( !$timestamp )
|
||||
{
|
||||
return 'Brak danych';
|
||||
}
|
||||
|
||||
return date( 'Y-m-d H:i:s', $timestamp );
|
||||
}
|
||||
|
||||
private static function get_base_url()
|
||||
{
|
||||
$scheme = ( !empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? ( $_SERVER['SERVER_NAME'] ?? 'localhost' );
|
||||
return $scheme . '://' . $host;
|
||||
}
|
||||
|
||||
public static function login()
|
||||
{
|
||||
if ( $user = \factory\Users::login(
|
||||
@@ -155,4 +470,4 @@ class Users
|
||||
return \Tpl::view( 'users/login-form' );
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
38
autoload/controls/class.XmlFiles.php
Normal file
38
autoload/controls/class.XmlFiles.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
namespace controls;
|
||||
|
||||
class XmlFiles
|
||||
{
|
||||
static public function main_view()
|
||||
{
|
||||
return \Tpl::view( 'xml_files/main_view', [
|
||||
'rows' => \factory\XmlFiles::get_clients_with_xml_feed()
|
||||
] );
|
||||
}
|
||||
|
||||
static public function regenerate()
|
||||
{
|
||||
$client_id = (int) \S::get( 'client_id' );
|
||||
|
||||
if ( $client_id <= 0 )
|
||||
{
|
||||
\S::alert( 'Nie podano ID klienta.' );
|
||||
header( 'Location: /xml_files' );
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = \controls\Cron::generate_custom_feed_for_client( $client_id, true );
|
||||
|
||||
if ( ( $result['status'] ?? '' ) === 'ok' )
|
||||
{
|
||||
\S::alert( 'Plik XML zostal wygenerowany: ' . ( $result['url'] ?? '' ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
\S::alert( $result['message'] ?? 'Nie udalo sie wygenerowac pliku XML.' );
|
||||
}
|
||||
|
||||
header( 'Location: /xml_files' );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,8 @@ class Campaigns
|
||||
st.id,
|
||||
st.campaign_id,
|
||||
st.ad_group_id,
|
||||
c.campaign_name,
|
||||
c.advertising_channel_type,
|
||||
ag.ad_group_name,
|
||||
st.search_term,
|
||||
st.impressions_30,
|
||||
@@ -122,6 +124,7 @@ class Campaigns
|
||||
st.conversion_value_all_time,
|
||||
st.roas_all_time
|
||||
FROM campaign_search_terms AS st
|
||||
LEFT JOIN campaigns AS c ON c.id = st.campaign_id
|
||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = st.ad_group_id
|
||||
WHERE st.campaign_id = :campaign_id';
|
||||
|
||||
@@ -167,6 +170,46 @@ class Campaigns
|
||||
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function get_campaign_keywords( $campaign_id, $ad_group_id = 0 )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$sql = 'SELECT
|
||||
kw.id,
|
||||
kw.campaign_id,
|
||||
kw.ad_group_id,
|
||||
ag.ad_group_name,
|
||||
kw.keyword_text,
|
||||
kw.match_type,
|
||||
kw.impressions_30,
|
||||
kw.clicks_30,
|
||||
kw.cost_30,
|
||||
kw.conversions_30,
|
||||
kw.conversion_value_30,
|
||||
kw.roas_30,
|
||||
kw.impressions_all_time,
|
||||
kw.clicks_all_time,
|
||||
kw.cost_all_time,
|
||||
kw.conversions_all_time,
|
||||
kw.conversion_value_all_time,
|
||||
kw.roas_all_time
|
||||
FROM campaign_keywords AS kw
|
||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = kw.ad_group_id
|
||||
WHERE kw.campaign_id = :campaign_id';
|
||||
|
||||
$params = [ ':campaign_id' => (int) $campaign_id ];
|
||||
|
||||
if ( (int) $ad_group_id > 0 )
|
||||
{
|
||||
$sql .= ' AND kw.ad_group_id = :ad_group_id';
|
||||
$params[':ad_group_id'] = (int) $ad_group_id;
|
||||
}
|
||||
|
||||
$sql .= ' ORDER BY kw.clicks_30 DESC, kw.clicks_all_time DESC, kw.keyword_text ASC';
|
||||
|
||||
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function get_search_term_context( $search_term_row_id )
|
||||
{
|
||||
global $mdb;
|
||||
@@ -242,6 +285,38 @@ class Campaigns
|
||||
return (int) $mdb -> id();
|
||||
}
|
||||
|
||||
static public function get_negative_keyword_context( $negative_keyword_row_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
return $mdb -> query(
|
||||
'SELECT
|
||||
nk.id AS negative_keyword_row_id,
|
||||
nk.scope,
|
||||
nk.keyword_text,
|
||||
nk.match_type,
|
||||
nk.campaign_id AS db_campaign_id,
|
||||
nk.ad_group_id AS db_ad_group_id,
|
||||
c.client_id,
|
||||
c.campaign_id AS external_campaign_id,
|
||||
ag.ad_group_id AS external_ad_group_id,
|
||||
cl.google_ads_customer_id
|
||||
FROM campaign_negative_keywords AS nk
|
||||
INNER JOIN campaigns AS c ON c.id = nk.campaign_id
|
||||
INNER JOIN clients AS cl ON cl.id = c.client_id
|
||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = nk.ad_group_id
|
||||
WHERE nk.id = :negative_keyword_row_id
|
||||
LIMIT 1',
|
||||
[ ':negative_keyword_row_id' => (int) $negative_keyword_row_id ]
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function delete_campaign_negative_keyword( $negative_keyword_row_id )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> delete( 'campaign_negative_keywords', [ 'id' => (int) $negative_keyword_row_id ] );
|
||||
}
|
||||
|
||||
static public function delete_campaign( $campaign_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
@@ -88,32 +88,64 @@ class Products
|
||||
$order_dir = strtoupper( (string) $order_dir ) === 'ASC' ? 'ASC' : 'DESC';
|
||||
|
||||
$order_map = [
|
||||
'offer_id' => 'p.offer_id',
|
||||
'campaign_name' => 'c.campaign_name',
|
||||
'ad_group_name' => 'ag.ad_group_name',
|
||||
'name' => 'pt.name',
|
||||
'impressions' => 'pt.impressions',
|
||||
'impressions_30' => 'pt.impressions_30',
|
||||
'clicks' => 'pt.clicks',
|
||||
'clicks_30' => 'pt.clicks_30',
|
||||
'ctr' => 'pt.ctr',
|
||||
'cost' => 'pt.cost',
|
||||
'cpc' => 'pt.cpc',
|
||||
'conversions' => 'pt.conversions',
|
||||
'conversions_value' => 'pt.conversions_value',
|
||||
'roas' => 'pt.roas',
|
||||
'min_roas' => 'p.min_roas'
|
||||
'offer_id' => 'offer_id',
|
||||
'campaign_name' => 'campaign_name',
|
||||
'ad_group_name' => 'ad_group_name',
|
||||
'name' => 'name',
|
||||
'impressions' => 'impressions',
|
||||
'impressions_30' => 'impressions_30',
|
||||
'clicks' => 'clicks',
|
||||
'clicks_30' => 'clicks_30',
|
||||
'ctr' => 'ctr',
|
||||
'cost' => 'cost',
|
||||
'cpc' => 'cpc',
|
||||
'conversions' => 'conversions',
|
||||
'conversions_value' => 'conversions_value',
|
||||
'roas' => 'roas',
|
||||
'min_roas' => 'min_roas'
|
||||
];
|
||||
|
||||
$order_sql = $order_map[ $order_name ] ?? 'pt.clicks';
|
||||
$order_sql = $order_map[ $order_name ] ?? 'clicks';
|
||||
|
||||
$params = [ ':client_id' => (int) $client_id ];
|
||||
$sql = 'SELECT pt.*, p.offer_id, p.min_roas,
|
||||
COALESCE( c.campaign_name, \'--- brak kampanii ---\' ) AS campaign_name,
|
||||
$sql = 'SELECT
|
||||
p.id AS product_id,
|
||||
p.offer_id,
|
||||
p.min_roas,
|
||||
pt.campaign_id,
|
||||
CASE
|
||||
WHEN pt.ad_group_id = 0 THEN \'PMax (bez grup reklam)\'
|
||||
ELSE COALESCE( ag.ad_group_name, \'--- brak grupy reklam ---\' )
|
||||
END AS ad_group_name
|
||||
WHEN COUNT( DISTINCT pt.campaign_id ) > 1 THEN \'--- wiele kampanii ---\'
|
||||
ELSE COALESCE( MAX( c.campaign_name ), \'--- brak kampanii ---\' )
|
||||
END AS campaign_name,
|
||||
CASE
|
||||
WHEN COUNT( DISTINCT pt.ad_group_id ) > 1 THEN \'--- wiele grup reklam ---\'
|
||||
WHEN MAX( pt.ad_group_id ) = 0 THEN \'PMax (bez grup reklam)\'
|
||||
ELSE COALESCE( MAX( ag.ad_group_name ), \'--- brak grupy reklam ---\' )
|
||||
END AS ad_group_name,
|
||||
CASE
|
||||
WHEN COUNT( DISTINCT pt.ad_group_id ) = 1 THEN MAX( pt.ad_group_id )
|
||||
ELSE 0
|
||||
END AS ad_group_id,
|
||||
MAX( pt.name ) AS name,
|
||||
SUM( pt.impressions ) AS impressions,
|
||||
SUM( pt.impressions_30 ) AS impressions_30,
|
||||
SUM( pt.clicks ) AS clicks,
|
||||
SUM( pt.clicks_30 ) AS clicks_30,
|
||||
CASE
|
||||
WHEN SUM( pt.impressions ) > 0 THEN ROUND( SUM( pt.clicks ) / SUM( pt.impressions ) * 100, 2 )
|
||||
ELSE 0
|
||||
END AS ctr,
|
||||
SUM( pt.cost ) AS cost,
|
||||
CASE
|
||||
WHEN SUM( pt.clicks ) > 0 THEN ROUND( SUM( pt.cost ) / SUM( pt.clicks ), 6 )
|
||||
ELSE 0
|
||||
END AS cpc,
|
||||
SUM( pt.conversions ) AS conversions,
|
||||
SUM( pt.conversions_value ) AS conversions_value,
|
||||
CASE
|
||||
WHEN SUM( pt.cost ) > 0 THEN ROUND( SUM( pt.conversions_value ) / SUM( pt.cost ) * 100, 2 )
|
||||
ELSE 0
|
||||
END AS roas
|
||||
FROM products_temp AS pt
|
||||
INNER JOIN products AS p ON p.id = pt.product_id
|
||||
LEFT JOIN campaigns AS c ON c.id = pt.campaign_id
|
||||
@@ -133,7 +165,8 @@ class Products
|
||||
$params[':search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
$sql .= ' ORDER BY ' . $order_sql . ' ' . $order_dir . ', pt.id DESC LIMIT ' . $start . ', ' . $limit;
|
||||
$sql .= ' GROUP BY p.id, p.offer_id, p.min_roas, pt.campaign_id';
|
||||
$sql .= ' ORDER BY ' . $order_sql . ' ' . $order_dir . ', product_id DESC LIMIT ' . $start . ', ' . $limit;
|
||||
|
||||
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
@@ -178,12 +211,14 @@ class Products
|
||||
global $mdb;
|
||||
|
||||
$params = [ ':client_id' => (int) $client_id ];
|
||||
$sql = 'SELECT COUNT(0)
|
||||
FROM products_temp AS pt
|
||||
INNER JOIN products AS p ON p.id = pt.product_id
|
||||
LEFT JOIN campaigns AS c ON c.id = pt.campaign_id
|
||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id
|
||||
WHERE p.client_id = :client_id';
|
||||
$sql = 'SELECT COUNT(0)
|
||||
FROM (
|
||||
SELECT p.id, pt.campaign_id
|
||||
FROM products_temp AS pt
|
||||
INNER JOIN products AS p ON p.id = pt.product_id
|
||||
LEFT JOIN campaigns AS c ON c.id = pt.campaign_id
|
||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id
|
||||
WHERE p.client_id = :client_id';
|
||||
|
||||
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
||||
|
||||
@@ -198,6 +233,9 @@ class Products
|
||||
$params[':search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
$sql .= ' GROUP BY p.id, pt.campaign_id
|
||||
) AS grouped_rows';
|
||||
|
||||
return $mdb -> query( $sql, $params ) -> fetchColumn();
|
||||
}
|
||||
|
||||
@@ -246,6 +284,177 @@ class Products
|
||||
return $mdb -> get( 'products_data', $field, [ 'product_id' => $product_id ] );
|
||||
}
|
||||
|
||||
static public function get_product_merchant_context( $product_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
return $mdb -> query(
|
||||
'SELECT
|
||||
p.id AS product_id,
|
||||
p.client_id,
|
||||
p.offer_id,
|
||||
cl.google_merchant_account_id
|
||||
FROM products AS p
|
||||
INNER JOIN clients AS cl ON cl.id = p.client_id
|
||||
WHERE p.id = :product_id
|
||||
LIMIT 1',
|
||||
[ ':product_id' => (int) $product_id ]
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function add_product_merchant_sync_log( $row )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$data = [
|
||||
'product_id' => (int) ( $row['product_id'] ?? 0 ),
|
||||
'field_name' => (string) ( $row['field_name'] ?? '' ),
|
||||
'old_value' => isset( $row['old_value'] ) ? (string) $row['old_value'] : null,
|
||||
'new_value' => isset( $row['new_value'] ) ? (string) $row['new_value'] : null,
|
||||
'sync_status' => (string) ( $row['sync_status'] ?? 'pending' ),
|
||||
'sync_source' => (string) ( $row['sync_source'] ?? 'products_ui' ),
|
||||
'merchant_account_id' => isset( $row['merchant_account_id'] ) ? (string) $row['merchant_account_id'] : null,
|
||||
'merchant_product_id' => isset( $row['merchant_product_id'] ) ? (string) $row['merchant_product_id'] : null,
|
||||
'offer_id' => isset( $row['offer_id'] ) ? (string) $row['offer_id'] : null,
|
||||
'api_response' => isset( $row['api_response'] ) ? (string) $row['api_response'] : null,
|
||||
'error_message' => isset( $row['error_message'] ) ? (string) $row['error_message'] : null,
|
||||
'date_add' => date( 'Y-m-d H:i:s' )
|
||||
];
|
||||
|
||||
if ( $data['product_id'] <= 0 || trim( $data['field_name'] ) === '' )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return $mdb -> insert( 'products_merchant_sync_log', $data );
|
||||
}
|
||||
|
||||
static public function get_product_merchant_sync_logs( $product_id, $limit = 50 )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$product_id = (int) $product_id;
|
||||
$limit = max( 1, (int) $limit );
|
||||
|
||||
if ( $product_id <= 0 )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return $mdb -> query(
|
||||
'SELECT
|
||||
id,
|
||||
field_name,
|
||||
old_value,
|
||||
new_value,
|
||||
sync_status,
|
||||
sync_source,
|
||||
merchant_account_id,
|
||||
merchant_product_id,
|
||||
offer_id,
|
||||
error_message,
|
||||
date_add
|
||||
FROM products_merchant_sync_log
|
||||
WHERE product_id = :product_id
|
||||
ORDER BY id DESC
|
||||
LIMIT ' . $limit,
|
||||
[ ':product_id' => $product_id ]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function get_product_ads_keyword_context( $product_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
return $mdb -> query(
|
||||
'SELECT
|
||||
p.id AS product_id,
|
||||
p.client_id,
|
||||
cl.google_ads_customer_id
|
||||
FROM products AS p
|
||||
INNER JOIN clients AS cl ON cl.id = p.client_id
|
||||
WHERE p.id = :product_id
|
||||
LIMIT 1',
|
||||
[ ':product_id' => (int) $product_id ]
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function get_cached_keyword_planner_terms( $product_id, $source_url, $limit = 15 )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$product_id = (int) $product_id;
|
||||
$source_url = trim( (string) $source_url );
|
||||
$limit = max( 1, (int) $limit );
|
||||
|
||||
if ( $product_id <= 0 || $source_url === '' )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return $mdb -> query(
|
||||
'SELECT keyword_text, avg_monthly_searches, competition, competition_index
|
||||
FROM products_keyword_planner_terms
|
||||
WHERE product_id = :product_id
|
||||
AND source_url = :source_url
|
||||
ORDER BY avg_monthly_searches DESC, keyword_text ASC
|
||||
LIMIT ' . $limit,
|
||||
[
|
||||
':product_id' => $product_id,
|
||||
':source_url' => $source_url
|
||||
]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function replace_keyword_planner_terms( $product_id, $source_url, $customer_id, $terms )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$product_id = (int) $product_id;
|
||||
$source_url = trim( (string) $source_url );
|
||||
$customer_id = trim( (string) $customer_id );
|
||||
$terms = is_array( $terms ) ? $terms : [];
|
||||
|
||||
if ( $product_id <= 0 || $source_url === '' )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$mdb -> delete( 'products_keyword_planner_terms', [
|
||||
'AND' => [
|
||||
'product_id' => $product_id,
|
||||
'source_url' => $source_url
|
||||
]
|
||||
] );
|
||||
|
||||
if ( empty( $terms ) )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ( $terms as $term )
|
||||
{
|
||||
$keyword_text = trim( (string) ( $term['keyword'] ?? '' ) );
|
||||
if ( $keyword_text === '' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$mdb -> insert( 'products_keyword_planner_terms', [
|
||||
'product_id' => $product_id,
|
||||
'source_url' => $source_url,
|
||||
'keyword_text' => mb_substr( $keyword_text, 0, 255 ),
|
||||
'avg_monthly_searches' => (int) ( $term['avg_monthly_searches'] ?? 0 ),
|
||||
'competition' => $term['competition'] ?? null,
|
||||
'competition_index' => isset( $term['competition_index'] ) ? (int) $term['competition_index'] : null,
|
||||
'source_customer_id' => $customer_id !== '' ? $customer_id : null,
|
||||
'date_add' => date( 'Y-m-d H:i:s' )
|
||||
] );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static public function set_product_data( $product_id, $field, $value )
|
||||
{
|
||||
global $mdb;
|
||||
@@ -406,4 +615,239 @@ class Products
|
||||
else
|
||||
return $mdb -> insert( 'products_comments', [ 'product_id' => $product_id, 'comment' => $comment, 'date_add' => $date ] );
|
||||
}
|
||||
|
||||
static public function get_product_scope_context( $product_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
return $mdb -> query(
|
||||
'SELECT
|
||||
p.id,
|
||||
p.client_id,
|
||||
p.offer_id,
|
||||
p.name,
|
||||
cl.google_ads_customer_id,
|
||||
cl.google_merchant_account_id
|
||||
FROM products AS p
|
||||
INNER JOIN clients AS cl ON cl.id = p.client_id
|
||||
WHERE p.id = :product_id
|
||||
LIMIT 1',
|
||||
[ ':product_id' => (int) $product_id ]
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function get_campaign_scope_context( $campaign_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
return $mdb -> query(
|
||||
'SELECT id, client_id, campaign_id, campaign_name
|
||||
FROM campaigns
|
||||
WHERE id = :campaign_id
|
||||
LIMIT 1',
|
||||
[ ':campaign_id' => (int) $campaign_id ]
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function get_ad_group_scope_context( $ad_group_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
return $mdb -> query(
|
||||
'SELECT id, campaign_id, ad_group_id, ad_group_name
|
||||
FROM campaign_ad_groups
|
||||
WHERE id = :ad_group_id
|
||||
LIMIT 1',
|
||||
[ ':ad_group_id' => (int) $ad_group_id ]
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static private function get_next_local_campaign_external_id( $client_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$min_external_id = (int) $mdb -> query(
|
||||
'SELECT MIN( campaign_id )
|
||||
FROM campaigns
|
||||
WHERE client_id = :client_id',
|
||||
[ ':client_id' => (int) $client_id ]
|
||||
) -> fetchColumn();
|
||||
|
||||
if ( $min_external_id < 0 )
|
||||
{
|
||||
return $min_external_id - 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static private function get_next_local_ad_group_external_id( $campaign_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$min_external_id = (int) $mdb -> query(
|
||||
'SELECT MIN( ad_group_id )
|
||||
FROM campaign_ad_groups
|
||||
WHERE campaign_id = :campaign_id',
|
||||
[ ':campaign_id' => (int) $campaign_id ]
|
||||
) -> fetchColumn();
|
||||
|
||||
if ( $min_external_id < 0 )
|
||||
{
|
||||
return $min_external_id - 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
static public function create_local_campaign( $client_id, $campaign_name )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$client_id = (int) $client_id;
|
||||
$campaign_name = trim( (string) $campaign_name );
|
||||
|
||||
if ( $client_id <= 0 || $campaign_name === '' )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
$existing_campaign_id = (int) $mdb -> get( 'campaigns', 'id', [
|
||||
'AND' => [
|
||||
'client_id' => $client_id,
|
||||
'campaign_name' => $campaign_name
|
||||
]
|
||||
] );
|
||||
|
||||
if ( $existing_campaign_id > 0 )
|
||||
{
|
||||
return $existing_campaign_id;
|
||||
}
|
||||
|
||||
$mdb -> insert( 'campaigns', [
|
||||
'client_id' => $client_id,
|
||||
'campaign_id' => self::get_next_local_campaign_external_id( $client_id ),
|
||||
'campaign_name' => $campaign_name
|
||||
] );
|
||||
|
||||
return (int) $mdb -> id();
|
||||
}
|
||||
|
||||
static public function create_local_ad_group( $campaign_id, $ad_group_name )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$campaign_id = (int) $campaign_id;
|
||||
$ad_group_name = trim( (string) $ad_group_name );
|
||||
|
||||
if ( $campaign_id <= 0 || $ad_group_name === '' )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
$existing_ad_group_id = (int) $mdb -> get( 'campaign_ad_groups', 'id', [
|
||||
'AND' => [
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_group_name' => $ad_group_name
|
||||
]
|
||||
] );
|
||||
|
||||
if ( $existing_ad_group_id > 0 )
|
||||
{
|
||||
return $existing_ad_group_id;
|
||||
}
|
||||
|
||||
$mdb -> insert( 'campaign_ad_groups', [
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_group_id' => self::get_next_local_ad_group_external_id( $campaign_id ),
|
||||
'ad_group_name' => $ad_group_name,
|
||||
'impressions_30' => 0,
|
||||
'clicks_30' => 0,
|
||||
'cost_30' => 0,
|
||||
'conversions_30' => 0,
|
||||
'conversion_value_30' => 0,
|
||||
'roas_30' => 0,
|
||||
'impressions_all_time' => 0,
|
||||
'clicks_all_time' => 0,
|
||||
'cost_all_time' => 0,
|
||||
'conversions_all_time' => 0,
|
||||
'conversion_value_all_time' => 0,
|
||||
'roas_all_time' => 0,
|
||||
'date_sync' => date( 'Y-m-d' )
|
||||
] );
|
||||
|
||||
return (int) $mdb -> id();
|
||||
}
|
||||
|
||||
static public function assign_product_scope( $product_id, $campaign_id, $ad_group_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$product_id = (int) $product_id;
|
||||
$campaign_id = (int) $campaign_id;
|
||||
$ad_group_id = (int) $ad_group_id;
|
||||
|
||||
if ( $product_id <= 0 || $campaign_id <= 0 || $ad_group_id <= 0 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$product = self::get_product_scope_context( $product_id );
|
||||
if ( !$product )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$campaign_client_id = (int) $mdb -> get( 'campaigns', 'client_id', [ 'id' => $campaign_id ] );
|
||||
if ( $campaign_client_id <= 0 || $campaign_client_id !== (int) $product['client_id'] )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$ad_group_campaign_id = (int) $mdb -> get( 'campaign_ad_groups', 'campaign_id', [ 'id' => $ad_group_id ] );
|
||||
if ( $ad_group_campaign_id <= 0 || $ad_group_campaign_id !== $campaign_id )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$scope_exists = (int) $mdb -> count( 'products_temp', [
|
||||
'AND' => [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_group_id' => $ad_group_id
|
||||
]
|
||||
] ) > 0;
|
||||
|
||||
if ( $scope_exists )
|
||||
{
|
||||
$mdb -> update( 'products_temp', [
|
||||
'name' => $product['name']
|
||||
], [
|
||||
'AND' => [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_group_id' => $ad_group_id
|
||||
]
|
||||
] );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return $mdb -> insert( 'products_temp', [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_group_id' => $ad_group_id,
|
||||
'name' => $product['name'],
|
||||
'impressions' => 0,
|
||||
'impressions_30' => 0,
|
||||
'clicks' => 0,
|
||||
'clicks_30' => 0,
|
||||
'ctr' => 0,
|
||||
'cost' => 0,
|
||||
'conversions' => 0,
|
||||
'conversions_value' => 0,
|
||||
'cpc' => 0,
|
||||
'roas' => 0
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
43
autoload/factory/class.XmlFiles.php
Normal file
43
autoload/factory/class.XmlFiles.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace factory;
|
||||
|
||||
class XmlFiles
|
||||
{
|
||||
static public function get_clients_with_xml_feed()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$clients = $mdb -> query(
|
||||
"SELECT id, name, google_ads_customer_id
|
||||
FROM clients
|
||||
WHERE deleted = 0
|
||||
ORDER BY name ASC"
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ( $clients as $client )
|
||||
{
|
||||
$client_id = (int) ( $client['id'] ?? 0 );
|
||||
$scheme = ( !empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? ( $_SERVER['SERVER_NAME'] ?? 'localhost' );
|
||||
$relative_path = '/xml/custom-feed-' . $client_id . '.xml';
|
||||
$absolute_url = $scheme . '://' . $host . $relative_path;
|
||||
$absolute_path = dirname( __DIR__, 2 ) . DIRECTORY_SEPARATOR . 'xml' . DIRECTORY_SEPARATOR . 'custom-feed-' . $client_id . '.xml';
|
||||
$exists = is_file( $absolute_path );
|
||||
$last_modified = $exists ? date( 'Y-m-d H:i:s', (int) filemtime( $absolute_path ) ) : '';
|
||||
|
||||
$rows[] = [
|
||||
'client_id' => $client_id,
|
||||
'client_name' => (string) ( $client['name'] ?? '' ),
|
||||
'google_ads_customer_id' => (string) ( $client['google_ads_customer_id'] ?? '' ),
|
||||
'xml_relative_path' => $relative_path,
|
||||
'xml_url' => $absolute_url,
|
||||
'xml_exists' => $exists,
|
||||
'xml_last_modified' => $last_modified
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
@@ -153,6 +153,31 @@ Twoje odpowiedzi muszą być:
|
||||
static public function suggest_title( $context )
|
||||
{
|
||||
$context_text = self::build_context_text( $context );
|
||||
$keyword_planner_text = '';
|
||||
|
||||
if ( !empty( $context['keyword_planner_terms'] ) && is_array( $context['keyword_planner_terms'] ) )
|
||||
{
|
||||
$keyword_lines = [];
|
||||
$keyword_lines[] = 'Najpopularniejsze frazy z Google Ads Keyword Planner (na bazie URL produktu, posortowane malejąco po średniej liczbie wyszukiwań):';
|
||||
|
||||
foreach ( array_slice( $context['keyword_planner_terms'], 0, 15 ) as $term )
|
||||
{
|
||||
$text = trim( (string) ( $term['keyword_text'] ?? '' ) );
|
||||
if ( $text === '' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$avg_monthly = (int) ( $term['avg_monthly_searches'] ?? 0 );
|
||||
$keyword_lines[] = '- ' . $text . ' (avg miesięcznie: ' . $avg_monthly . ')';
|
||||
}
|
||||
|
||||
if ( count( $keyword_lines ) > 1 )
|
||||
{
|
||||
$keyword_lines[] = 'Użyj tych fraz WYBIÓRCZO i naturalnie (bez upychania słów kluczowych), tylko jeśli pasują do produktu.';
|
||||
$keyword_planner_text = "\n\n" . implode( "\n", $keyword_lines );
|
||||
}
|
||||
}
|
||||
|
||||
$prompt = 'Zaproponuj zoptymalizowany tytuł produktu dla Google Merchant Center.
|
||||
|
||||
@@ -170,7 +195,7 @@ BEZWZGLĘDNY ZAKAZ (odrzucenie przez Google):
|
||||
- Cechy wymyślone — opisuj TYLKO to co wynika z oryginalnej nazwy lub treści strony produktu
|
||||
- Jeśli podano treść ze strony produktu, wykorzystaj ją do wzbogacenia tytułu o rzeczywiste cechy (marka, materiał, kolor, rozmiar itp.)
|
||||
|
||||
' . $context_text . '
|
||||
' . $context_text . $keyword_planner_text . '
|
||||
|
||||
Zwróć TYLKO tytuł, bez cudzysłowów, bez wyjaśnień.';
|
||||
|
||||
@@ -181,6 +206,31 @@ Zwróć TYLKO tytuł, bez cudzysłowów, bez wyjaśnień.';
|
||||
{
|
||||
$context_text = self::build_context_text( $context );
|
||||
$has_page = !empty( $context['page_content'] );
|
||||
$keyword_planner_text = '';
|
||||
|
||||
if ( !empty( $context['keyword_planner_terms'] ) && is_array( $context['keyword_planner_terms'] ) )
|
||||
{
|
||||
$keyword_lines = [];
|
||||
$keyword_lines[] = 'Najpopularniejsze frazy z Google Ads Keyword Planner (na bazie URL produktu, posortowane malejąco po średniej liczbie wyszukiwań):';
|
||||
|
||||
foreach ( array_slice( $context['keyword_planner_terms'], 0, 15 ) as $term )
|
||||
{
|
||||
$text = trim( (string) ( $term['keyword_text'] ?? '' ) );
|
||||
if ( $text === '' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$avg_monthly = (int) ( $term['avg_monthly_searches'] ?? 0 );
|
||||
$keyword_lines[] = '- ' . $text . ' (avg miesięcznie: ' . $avg_monthly . ')';
|
||||
}
|
||||
|
||||
if ( count( $keyword_lines ) > 1 )
|
||||
{
|
||||
$keyword_lines[] = 'W opisie wykorzystuj te frazy naturalnie i wyłącznie gdy realnie pasują do produktu (bez keyword stuffing).';
|
||||
$keyword_planner_text = "\n\n" . implode( "\n", $keyword_lines );
|
||||
}
|
||||
}
|
||||
|
||||
$length_guide = $has_page
|
||||
? '- Napisz rozbudowany, szczegółowy opis: ok. 1000 znaków (800-1200)
|
||||
@@ -213,7 +263,7 @@ BEZWZGLĘDNY ZAKAZ (odrzucenie przez Google):
|
||||
- Opisy akcesoriów/produktów nie wchodzących w skład oferty
|
||||
- Cechy wymyślone — opisuj TYLKO to co wynika z nazwy lub treści strony produktu
|
||||
|
||||
' . $context_text . '
|
||||
' . $context_text . $keyword_planner_text . '
|
||||
|
||||
Zwróć TYLKO opis w formacie HTML (używając dozwolonych tagów), bez cudzysłowów, bez wyjaśnień.';
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -98,13 +98,14 @@ Twoje odpowiedzi muszą być:
|
||||
return $text;
|
||||
}
|
||||
|
||||
static private function call_api( $system_prompt, $user_prompt, $max_tokens = 500 )
|
||||
static private function call_api( $system_prompt, $user_prompt, $max_tokens = 500, $temperature = 0.7, $extra_payload = [] )
|
||||
{
|
||||
$api_key = GoogleAdsApi::get_setting( 'openai_api_key' );
|
||||
$model = GoogleAdsApi::get_setting( 'openai_model' ) ?: 'gpt-5-mini';
|
||||
$is_gpt5_model = ( strpos( $model, 'gpt-5' ) === 0 );
|
||||
|
||||
// GPT-5.x wymaga max_completion_tokens, starsze modele używają max_tokens
|
||||
$tokens_key = ( strpos( $model, 'gpt-5' ) === 0 ) ? 'max_completion_tokens' : 'max_tokens';
|
||||
$tokens_key = $is_gpt5_model ? 'max_completion_tokens' : 'max_tokens';
|
||||
|
||||
$payload = [
|
||||
'model' => $model,
|
||||
@@ -112,10 +113,20 @@ Twoje odpowiedzi muszą być:
|
||||
[ 'role' => 'system', 'content' => $system_prompt ],
|
||||
[ 'role' => 'user', 'content' => $user_prompt ]
|
||||
],
|
||||
'temperature' => 0.7,
|
||||
$tokens_key => $max_tokens
|
||||
];
|
||||
|
||||
// Modele GPT-5 (w tym gpt-5-mini) nie wspierają niestandardowej temperatury.
|
||||
if ( !$is_gpt5_model )
|
||||
{
|
||||
$payload['temperature'] = $temperature;
|
||||
}
|
||||
|
||||
if ( is_array( $extra_payload ) && !empty( $extra_payload ) )
|
||||
{
|
||||
$payload = array_merge( $payload, $extra_payload );
|
||||
}
|
||||
|
||||
$ch = curl_init( self::$api_url );
|
||||
curl_setopt_array( $ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
@@ -187,6 +198,31 @@ Twoje odpowiedzi muszą być:
|
||||
static public function suggest_title( $context )
|
||||
{
|
||||
$context_text = self::build_context_text( $context );
|
||||
$keyword_planner_text = '';
|
||||
|
||||
if ( !empty( $context['keyword_planner_terms'] ) && is_array( $context['keyword_planner_terms'] ) )
|
||||
{
|
||||
$keyword_lines = [];
|
||||
$keyword_lines[] = 'Najpopularniejsze frazy z Google Ads Keyword Planner (na bazie URL produktu, posortowane malejąco po średniej liczbie wyszukiwań):';
|
||||
|
||||
foreach ( array_slice( $context['keyword_planner_terms'], 0, 15 ) as $term )
|
||||
{
|
||||
$text = trim( (string) ( $term['keyword_text'] ?? '' ) );
|
||||
if ( $text === '' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$avg_monthly = (int) ( $term['avg_monthly_searches'] ?? 0 );
|
||||
$keyword_lines[] = '- ' . $text . ' (avg miesięcznie: ' . $avg_monthly . ')';
|
||||
}
|
||||
|
||||
if ( count( $keyword_lines ) > 1 )
|
||||
{
|
||||
$keyword_lines[] = 'Użyj tych fraz WYBIÓRCZO i naturalnie (bez upychania słów kluczowych), tylko jeśli pasują do produktu.';
|
||||
$keyword_planner_text = "\n\n" . implode( "\n", $keyword_lines );
|
||||
}
|
||||
}
|
||||
|
||||
$prompt = 'Zaproponuj zoptymalizowany tytuł produktu dla Google Merchant Center.
|
||||
|
||||
@@ -204,7 +240,7 @@ BEZWZGLĘDNY ZAKAZ (odrzucenie przez Google):
|
||||
- Cechy wymyślone — opisuj TYLKO to co wynika z oryginalnej nazwy lub treści strony produktu
|
||||
- Jeśli podano treść ze strony produktu, wykorzystaj ją do wzbogacenia tytułu o rzeczywiste cechy (marka, materiał, kolor, rozmiar itp.)
|
||||
|
||||
' . $context_text . '
|
||||
' . $context_text . $keyword_planner_text . '
|
||||
|
||||
Zwróć TYLKO tytuł, bez cudzysłowów, bez wyjaśnień.';
|
||||
|
||||
@@ -215,6 +251,31 @@ Zwróć TYLKO tytuł, bez cudzysłowów, bez wyjaśnień.';
|
||||
{
|
||||
$context_text = self::build_context_text( $context );
|
||||
$has_page = !empty( $context['page_content'] );
|
||||
$keyword_planner_text = '';
|
||||
|
||||
if ( !empty( $context['keyword_planner_terms'] ) && is_array( $context['keyword_planner_terms'] ) )
|
||||
{
|
||||
$keyword_lines = [];
|
||||
$keyword_lines[] = 'Najpopularniejsze frazy z Google Ads Keyword Planner (na bazie URL produktu, posortowane malejąco po średniej liczbie wyszukiwań):';
|
||||
|
||||
foreach ( array_slice( $context['keyword_planner_terms'], 0, 15 ) as $term )
|
||||
{
|
||||
$text = trim( (string) ( $term['keyword_text'] ?? '' ) );
|
||||
if ( $text === '' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$avg_monthly = (int) ( $term['avg_monthly_searches'] ?? 0 );
|
||||
$keyword_lines[] = '- ' . $text . ' (avg miesięcznie: ' . $avg_monthly . ')';
|
||||
}
|
||||
|
||||
if ( count( $keyword_lines ) > 1 )
|
||||
{
|
||||
$keyword_lines[] = 'W opisie wykorzystuj te frazy naturalnie i wyłącznie gdy realnie pasują do produktu (bez keyword stuffing).';
|
||||
$keyword_planner_text = "\n\n" . implode( "\n", $keyword_lines );
|
||||
}
|
||||
}
|
||||
|
||||
$length_guide = $has_page
|
||||
? '- Napisz rozbudowany, szczegółowy opis: ok. 1000 znaków (800-1200)
|
||||
@@ -247,7 +308,7 @@ BEZWZGLĘDNY ZAKAZ (odrzucenie przez Google):
|
||||
- Opisy akcesoriów/produktów nie wchodzących w skład oferty
|
||||
- Cechy wymyślone — opisuj TYLKO to co wynika z nazwy lub treści strony produktu
|
||||
|
||||
' . $context_text . '
|
||||
' . $context_text . $keyword_planner_text . '
|
||||
|
||||
Zwróć TYLKO opis w formacie HTML (używając dozwolonych tagów), bez cudzysłowów, bez wyjaśnień.';
|
||||
|
||||
@@ -279,4 +340,118 @@ Zwróć TYLKO ID kategorii (liczbę), bez wyjaśnień.';
|
||||
|
||||
return self::call_api( self::$system_prompt, $prompt );
|
||||
}
|
||||
|
||||
static public function suggest_negative_keywords_to_exclude( $search_terms_rows, $analysis_context = [] )
|
||||
{
|
||||
$rows = is_array( $search_terms_rows ) ? $search_terms_rows : [];
|
||||
$rows = array_slice( $rows, 0, 150 );
|
||||
|
||||
$campaign_name = trim( (string) ( $analysis_context['campaign_name'] ?? '' ) );
|
||||
$campaign_type = trim( (string) ( $analysis_context['campaign_type'] ?? '' ) );
|
||||
$ad_group_name = trim( (string) ( $analysis_context['ad_group_name'] ?? '' ) );
|
||||
$ad_group_id = (int) ( $analysis_context['ad_group_id'] ?? 0 );
|
||||
|
||||
$context_lines = [];
|
||||
$context_lines[] = 'KONTEKST KAMPANII:';
|
||||
$context_lines[] = '- Nazwa kampanii: ' . ( $campaign_name !== '' ? $campaign_name : '-' );
|
||||
$context_lines[] = '- Rodzaj kampanii: ' . ( $campaign_type !== '' ? $campaign_type : '-' );
|
||||
if ( $ad_group_id > 0 || $ad_group_name !== '' )
|
||||
{
|
||||
$context_lines[] = '- Wybrana grupa reklam: ' . ( $ad_group_name !== '' ? $ad_group_name : ( '#' . $ad_group_id ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$context_lines[] = '- Wybrana grupa reklam: wszystkie';
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
foreach ( $rows as $row )
|
||||
{
|
||||
$id = (int) ( $row['id'] ?? 0 );
|
||||
if ( $id <= 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$lines[] = json_encode( [
|
||||
'id' => $id,
|
||||
'phrase' => trim( (string) ( $row['search_term'] ?? '' ) ),
|
||||
'ad_group' => trim( (string) ( $row['ad_group_name'] ?? '' ) ),
|
||||
'clicks_all' => (float) ( $row['clicks_all_time'] ?? 0 ),
|
||||
'cost_all' => (float) ( $row['cost_all_time'] ?? 0 ),
|
||||
'value_all' => (float) ( $row['conversion_value_all_time'] ?? 0 ),
|
||||
'roas_all' => (float) ( $row['roas_all_time'] ?? 0 ),
|
||||
'clicks_30' => (float) ( $row['clicks_30'] ?? 0 ),
|
||||
'cost_30' => (float) ( $row['cost_30'] ?? 0 ),
|
||||
'value_30' => (float) ( $row['conversion_value_30'] ?? 0 ),
|
||||
'roas_30' => (float) ( $row['roas_30'] ?? 0 )
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
|
||||
}
|
||||
|
||||
$prompt = 'Przeanalizuj frazy wyszukiwane Google Ads i wskaż, które warto wykluczyć jako negatywne słowa kluczowe.
|
||||
|
||||
' . implode( "\n", $context_lines ) . '
|
||||
|
||||
KRYTERIA OCENY:
|
||||
- Priorytet: frazy z kosztami i kliknięciami bez wartości konwersji, niski/zerowy ROAS, nietrafna intencja.
|
||||
- Nie zaznaczaj wszystkich na siłę. Jeśli fraza ma potencjał, ustaw akcję "keep".
|
||||
- Oceniaj zarówno dane all-time, jak i 30d.
|
||||
- Powód musi być krótki, konkretny i oparty na danych.
|
||||
|
||||
FORMAT ODPOWIEDZI (BEZWZGLĘDNIE):
|
||||
Zwróć WYŁĄCZNIE poprawny JSON (bez markdown i bez komentarzy), zgodny ze schematem:
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 123,
|
||||
"phrase": "fraza",
|
||||
"action": "exclude" lub "keep",
|
||||
"reason": "krótki powód"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Zasady formatu:
|
||||
- Pole id musi być identyczne z wejściowym id.
|
||||
- action może mieć tylko wartości: "exclude" albo "keep".
|
||||
- reason max 120 znaków.
|
||||
- Nie dodawaj żadnych dodatkowych pól.
|
||||
|
||||
DANE WEJŚCIOWE (JSONL, 1 rekord na linię):
|
||||
' . implode( "\n", $lines );
|
||||
|
||||
$schema = [
|
||||
'type' => 'json_schema',
|
||||
'json_schema' => [
|
||||
'name' => 'negative_keyword_recommendations',
|
||||
'schema' => [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'items' => [
|
||||
'type' => 'array',
|
||||
'items' => [
|
||||
'type' => 'object',
|
||||
'additionalProperties' => false,
|
||||
'properties' => [
|
||||
'id' => [ 'type' => 'integer' ],
|
||||
'phrase' => [ 'type' => 'string' ],
|
||||
'action' => [ 'type' => 'string', 'enum' => [ 'exclude', 'keep' ] ],
|
||||
'reason' => [ 'type' => 'string' ]
|
||||
],
|
||||
'required' => [ 'id', 'phrase', 'action', 'reason' ]
|
||||
]
|
||||
]
|
||||
],
|
||||
'required' => [ 'items' ]
|
||||
],
|
||||
'strict' => true
|
||||
]
|
||||
];
|
||||
|
||||
$rows_count = count( $rows );
|
||||
$max_tokens = min( 6000, max( 2200, $rows_count * 30 ) );
|
||||
|
||||
return self::call_api( self::$system_prompt, $prompt, $max_tokens, 0.2, [ 'response_format' => $schema ] );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@ class Users
|
||||
return $tpl -> render( 'users/points-history' );
|
||||
}
|
||||
|
||||
public static function settings( $user )
|
||||
public static function settings( $user, $cron_data = [] )
|
||||
{
|
||||
$tpl = new \Tpl;
|
||||
$tpl -> user = $user;
|
||||
$tpl -> cron_data = $cron_data;
|
||||
return $tpl -> render( 'users/settings' );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ $database['name'] = 'host700513_adspro';
|
||||
$database['host'] = 'localhost';
|
||||
$database['user'] = 'host700513_adspro';
|
||||
$database['password'] = '2Ug7DvBy5MCAJtKmkCRs';
|
||||
$database['remote_host'] = 'host700513.hostido.net.pl';
|
||||
|
||||
$settings['email_host'] = 'mail.project-pro.pl';
|
||||
$settings['email_port'] = 25;
|
||||
$settings['email_login'] = 'www@project-pro.pl';
|
||||
$settings['email_password'] = 'ProjectPro2025!';
|
||||
|
||||
$settings['cron_products_clients_per_run'] = 1;
|
||||
$settings['cron_campaigns_clients_per_run'] = 1;
|
||||
|
||||
4
cron.php
4
cron.php
@@ -43,6 +43,10 @@ $mdb = new medoo( [
|
||||
'charset' => 'utf8'
|
||||
] );
|
||||
|
||||
$cron_now = date( 'Y-m-d H:i:s' );
|
||||
\services\GoogleAdsApi::set_setting( 'cron_last_invoked_at', $cron_now );
|
||||
\services\GoogleAdsApi::set_setting( 'cron_last_invoked_cron_legacy_at', $cron_now );
|
||||
|
||||
\R::setup( 'mysql:host=' . $database['host'] . ';dbname=' . $database['name'], $database['user'], $database['password'] );
|
||||
\R::ext( 'xdispense', function( $type )
|
||||
{
|
||||
|
||||
@@ -63,6 +63,7 @@ CREATE TABLE IF NOT EXISTS `clients` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL DEFAULT '0',
|
||||
`google_ads_customer_id` varchar(20) DEFAULT NULL,
|
||||
`google_merchant_account_id` varchar(32) DEFAULT NULL,
|
||||
`google_ads_start_date` date DEFAULT NULL,
|
||||
`deleted` int(11) DEFAULT 0,
|
||||
`bestseller_min_roas` int(11) DEFAULT NULL,
|
||||
@@ -330,6 +331,31 @@ CREATE TABLE IF NOT EXISTS `campaign_search_terms` (
|
||||
KEY `idx_campaign_search_terms_ad_group_id` (`ad_group_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `campaign_keywords` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`campaign_id` int(11) NOT NULL,
|
||||
`ad_group_id` int(11) NOT NULL,
|
||||
`keyword_text` varchar(255) NOT NULL,
|
||||
`match_type` varchar(40) DEFAULT NULL,
|
||||
`impressions_30` int(11) NOT NULL DEFAULT 0,
|
||||
`clicks_30` int(11) NOT NULL DEFAULT 0,
|
||||
`cost_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`conversions_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`conversion_value_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`roas_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`impressions_all_time` int(11) NOT NULL DEFAULT 0,
|
||||
`clicks_all_time` int(11) NOT NULL DEFAULT 0,
|
||||
`cost_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`conversions_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`conversion_value_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`roas_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`date_sync` date DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_campaign_keywords` (`campaign_id`,`ad_group_id`,`keyword_text`(191),`match_type`),
|
||||
KEY `idx_campaign_keywords_campaign_id` (`campaign_id`),
|
||||
KEY `idx_campaign_keywords_ad_group_id` (`ad_group_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `campaign_negative_keywords` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`campaign_id` int(11) NOT NULL,
|
||||
|
||||
@@ -36,6 +36,12 @@ Ten plik sluzy jako trwala pamiec dla Claude Code. Zapisuj tu wzorce, decyzje i
|
||||
- Frazy wyszukiwane dodane do wykluczonych oznaczane czerwonym kolorem (klasa CSS `term-is-negative`)
|
||||
- Negatywne slowa kluczowe dodawane przez Google Ads API i zapisywane lokalnie w `campaign_negative_keywords`
|
||||
- Klucze API przechowywane w tabeli `settings` (key-value)
|
||||
- Frazy z Google Ads Keyword Planner dla URL produktu sa cachowane w `products_keyword_planner_terms` i ponownie uzywane przy generowaniu tytulu AI
|
||||
- Zmiany produktowe (`title`, `description`, `google_product_category`, `custom_label_4`) sa synchronizowane bezposrednio do Merchant API i logowane per pole w `products_merchant_sync_log`
|
||||
- `cron_products` dziala batchowo po klientach (`clients_per_run`), domyslnie `10` (max `100`), aby ograniczyc liczbe wywolan; odpowiedz zawiera `estimated_calls_remaining_in_phase`
|
||||
- `cron_campaigns` dziala batchowo po klientach (`clients_per_run`), domyslnie `2` (max `20`), a odpowiedz zawiera `estimated_calls_remaining_today`
|
||||
- Zmiana listy klientow (np. reaktywacja klienta) nie powinna resetowac postepu `cron_products`; pipeline zachowuje przetworzonych i dopina nowych klientow
|
||||
- W `cron_campaigns` stan `processed_ids` jest normalizowany do aktualnej listy aktywnych klientow, aby uniknac rozjazdow postepu po zmianach aktywnosci
|
||||
|
||||
## Preferencje uzytkownika
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ $route_aliases = [
|
||||
'logout' => ['users', 'logout'],
|
||||
'settings' => ['users', 'settings'],
|
||||
'settings/save' => ['users', 'settings_save'],
|
||||
'settings/cron_status' => ['users', 'settings_cron_status'],
|
||||
'settings/save_google_ads' => ['users', 'settings_save_google_ads'],
|
||||
'settings/save_openai' => ['users', 'settings_save_openai'],
|
||||
'settings/save_claude' => ['users', 'settings_save_claude'],
|
||||
|
||||
1680
layout/style.css
1680
layout/style.css
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1137
layout/style.scss
1137
layout/style.scss
File diff suppressed because it is too large
Load Diff
@@ -188,7 +188,12 @@
|
||||
line-height: 1.4;
|
||||
}
|
||||
.adspro-dialog-btn:focus {
|
||||
outline: none;
|
||||
outline: 2px solid #6690F4;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.adspro-dialog-btn:focus-visible {
|
||||
outline: 2px solid #6690F4;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Klasy przycisków */
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
this._appendToBody();
|
||||
this._applyAutoClose();
|
||||
this._triggerContentReady();
|
||||
this._focusInitialElement();
|
||||
activeDialogs.push( this );
|
||||
},
|
||||
|
||||
@@ -72,7 +73,7 @@
|
||||
'<div class="adspro-dialog-bg"></div>' +
|
||||
'<div class="adspro-dialog-scrollpane">' +
|
||||
'<div class="adspro-dialog-center' + sizeClass + '">' +
|
||||
'<div class="adspro-dialog-box jconfirm-box' + typeClass + '" style="' + sizeStyle + '">' +
|
||||
'<div class="adspro-dialog-box jconfirm-box' + typeClass + '" style="' + sizeStyle + '" tabindex="-1">' +
|
||||
this._buildCloseIcon( o ) +
|
||||
this._buildHeader( o ) +
|
||||
'<div class="adspro-dialog-content-pane">' +
|
||||
@@ -199,7 +200,7 @@
|
||||
}
|
||||
if ( e.key === 'Enter' )
|
||||
{
|
||||
if ( $( e.target ).is( 'textarea, select' ) ) return;
|
||||
if ( $( e.target ).is( '.adspro-dialog-btn, textarea, select, input' ) ) return;
|
||||
var $enterBtn = self.$el.find( '.adspro-dialog-btn[data-enter-key="true"]' );
|
||||
if ( $enterBtn.length )
|
||||
{
|
||||
@@ -262,6 +263,40 @@
|
||||
}
|
||||
},
|
||||
|
||||
_focusInitialElement: function()
|
||||
{
|
||||
var self = this;
|
||||
|
||||
setTimeout( function()
|
||||
{
|
||||
if ( self._closed ) return;
|
||||
if ( $( document.activeElement ).closest( self.$el ).length ) return;
|
||||
|
||||
var $focusTarget = self.$el.find( '[autofocus]:visible:not(:disabled):first' );
|
||||
if ( !$focusTarget.length )
|
||||
{
|
||||
$focusTarget = self.$el.find( '.adspro-dialog-content input:visible:not(:disabled):first, .adspro-dialog-content select:visible:not(:disabled):first, .adspro-dialog-content textarea:visible:not(:disabled):first' ).first();
|
||||
}
|
||||
if ( !$focusTarget.length )
|
||||
{
|
||||
$focusTarget = self.$el.find( '.adspro-dialog-btn[data-enter-key="true"]:visible:not(:disabled):first' );
|
||||
}
|
||||
if ( !$focusTarget.length )
|
||||
{
|
||||
$focusTarget = self.$el.find( '.adspro-dialog-btn:visible:not(:disabled):first' );
|
||||
}
|
||||
if ( !$focusTarget.length )
|
||||
{
|
||||
$focusTarget = self.$box;
|
||||
}
|
||||
|
||||
if ( $focusTarget.length )
|
||||
{
|
||||
$focusTarget.trigger( 'focus' );
|
||||
}
|
||||
}, 20 );
|
||||
},
|
||||
|
||||
// --- Metody publiczne ---
|
||||
|
||||
close: function()
|
||||
|
||||
2
migrations/007_clients_merchant_account_id.sql
Normal file
2
migrations/007_clients_merchant_account_id.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `clients`
|
||||
ADD COLUMN `google_merchant_account_id` varchar(32) DEFAULT NULL AFTER `google_ads_customer_id`;
|
||||
15
migrations/008_products_keyword_planner_terms.sql
Normal file
15
migrations/008_products_keyword_planner_terms.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE IF NOT EXISTS `products_keyword_planner_terms` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`product_id` INT(11) NOT NULL,
|
||||
`source_url` VARCHAR(1024) NOT NULL,
|
||||
`keyword_text` VARCHAR(255) NOT NULL,
|
||||
`avg_monthly_searches` INT(11) NOT NULL DEFAULT 0,
|
||||
`competition` VARCHAR(32) DEFAULT NULL,
|
||||
`competition_index` INT(11) DEFAULT NULL,
|
||||
`source_customer_id` VARCHAR(32) DEFAULT NULL,
|
||||
`date_add` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_products_keyword_planner_terms` (`product_id`,`source_url`(255),`keyword_text`),
|
||||
KEY `idx_products_keyword_product` (`product_id`),
|
||||
KEY `idx_products_keyword_popularity` (`product_id`,`avg_monthly_searches`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
20
migrations/009_products_merchant_sync_log.sql
Normal file
20
migrations/009_products_merchant_sync_log.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
CREATE TABLE IF NOT EXISTS `products_merchant_sync_log` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`product_id` INT(11) NOT NULL,
|
||||
`field_name` VARCHAR(64) NOT NULL,
|
||||
`old_value` TEXT NULL,
|
||||
`new_value` TEXT NULL,
|
||||
`sync_status` VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
`sync_source` VARCHAR(32) NOT NULL DEFAULT 'products_ui',
|
||||
`merchant_account_id` VARCHAR(32) NULL,
|
||||
`merchant_product_id` VARCHAR(255) NULL,
|
||||
`offer_id` VARCHAR(255) NULL,
|
||||
`api_response` MEDIUMTEXT NULL,
|
||||
`error_message` TEXT NULL,
|
||||
`date_add` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_products_merchant_sync_product` (`product_id`),
|
||||
KEY `idx_products_merchant_sync_field` (`field_name`),
|
||||
KEY `idx_products_merchant_sync_status` (`sync_status`),
|
||||
KEY `idx_products_merchant_sync_date` (`date_add`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
24
migrations/010_campaign_keywords.sql
Normal file
24
migrations/010_campaign_keywords.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE IF NOT EXISTS `campaign_keywords` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`campaign_id` int(11) NOT NULL,
|
||||
`ad_group_id` int(11) NOT NULL,
|
||||
`keyword_text` varchar(255) NOT NULL,
|
||||
`match_type` varchar(40) DEFAULT NULL,
|
||||
`impressions_30` int(11) NOT NULL DEFAULT 0,
|
||||
`clicks_30` int(11) NOT NULL DEFAULT 0,
|
||||
`cost_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`conversions_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`conversion_value_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`roas_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`impressions_all_time` int(11) NOT NULL DEFAULT 0,
|
||||
`clicks_all_time` int(11) NOT NULL DEFAULT 0,
|
||||
`cost_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`conversions_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`conversion_value_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`roas_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`date_sync` date DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_campaign_keywords` (`campaign_id`,`ad_group_id`,`keyword_text`(191),`match_type`),
|
||||
KEY `idx_campaign_keywords_campaign_id` (`campaign_id`),
|
||||
KEY `idx_campaign_keywords_ad_group_id` (`ad_group_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@
|
||||
<th style="width: 60px;">#ID</th>
|
||||
<th>Nazwa klienta</th>
|
||||
<th>Google Ads Customer ID</th>
|
||||
<th>Merchant Account ID</th>
|
||||
<th>Dane od</th>
|
||||
<th style="width: 120px; text-align: center;">Akcje</th>
|
||||
</tr>
|
||||
@@ -30,6 +31,13 @@
|
||||
<span class="text-muted">— brak —</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ( !empty( $client['google_merchant_account_id'] ) ): ?>
|
||||
<span class="badge-id"><?= htmlspecialchars( $client['google_merchant_account_id'] ); ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">— brak —</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ( $client['google_ads_start_date'] ): ?>
|
||||
<?= $client['google_ads_start_date']; ?>
|
||||
@@ -49,7 +57,7 @@
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="5" class="empty-state">
|
||||
<td colspan="6" class="empty-state">
|
||||
<i class="fa-solid fa-building"></i>
|
||||
<p>Brak klientów. Dodaj pierwszego klienta.</p>
|
||||
</td>
|
||||
@@ -78,6 +86,11 @@
|
||||
<label for="client-gads-id">Google Ads Customer ID</label>
|
||||
<input type="text" id="client-gads-id" name="google_ads_customer_id" class="form-control" placeholder="np. 123-456-7890 (opcjonalnie)" />
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="client-gmc-id">Merchant Account ID</label>
|
||||
<input type="text" id="client-gmc-id" name="google_merchant_account_id" class="form-control" placeholder="np. 123456789 (opcjonalnie)" />
|
||||
<small class="text-muted">ID konta Merchant Center przypisane do klienta</small>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="client-gads-start">Pobieraj dane od</label>
|
||||
<input type="date" id="client-gads-start" name="google_ads_start_date" class="form-control" />
|
||||
@@ -99,6 +112,7 @@ function openClientForm()
|
||||
$( '#client-id' ).val( '' );
|
||||
$( '#client-name' ).val( '' );
|
||||
$( '#client-gads-id' ).val( '' );
|
||||
$( '#client-gmc-id' ).val( '' );
|
||||
$( '#client-gads-start' ).val( '' );
|
||||
$( '#client-modal' ).fadeIn();
|
||||
}
|
||||
@@ -115,6 +129,7 @@ function editClient( id )
|
||||
$( '#client-id' ).val( data.id );
|
||||
$( '#client-name' ).val( data.name );
|
||||
$( '#client-gads-id' ).val( data.google_ads_customer_id || '' );
|
||||
$( '#client-gmc-id' ).val( data.google_merchant_account_id || '' );
|
||||
$( '#client-gads-start' ).val( data.google_ads_start_date || '' );
|
||||
$( '#client-modal' ).fadeIn();
|
||||
} );
|
||||
@@ -126,10 +141,17 @@ function deleteClient( id, name )
|
||||
title: 'Potwierdzenie',
|
||||
content: 'Czy na pewno chcesz usunąć klienta <strong>' + name + '</strong>?',
|
||||
type: 'red',
|
||||
onContentReady: function() {
|
||||
if ( this['$$confirm'] && this['$$confirm'].length )
|
||||
{
|
||||
this['$$confirm'].trigger( 'focus' );
|
||||
}
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: 'Usuń',
|
||||
btnClass: 'btn-red',
|
||||
keys: [ 'enter' ],
|
||||
action: function() {
|
||||
$.post( '/clients/delete', { id: id }, function( response ) {
|
||||
var data = JSON.parse( response );
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
<th>Id oferty</th>
|
||||
<th>Kampania</th>
|
||||
<th>Grupa reklam</th>
|
||||
<th>URL</th>
|
||||
<th>Nazwa produktu</th>
|
||||
<th>Wyśw.</th>
|
||||
<th>Wyśw. (30d)</th>
|
||||
@@ -83,63 +84,11 @@
|
||||
$openai_enabled = \services\GoogleAdsApi::get_setting( 'openai_enabled' ) !== '0';
|
||||
$claude_enabled = \services\GoogleAdsApi::get_setting( 'claude_enabled' ) !== '0';
|
||||
?>
|
||||
<style>
|
||||
.products-page .products-filters .filter-group.filter-group-columns {
|
||||
min-width: 240px;
|
||||
}
|
||||
.products-columns-control {
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 6px;
|
||||
background: #FFFFFF;
|
||||
overflow: hidden;
|
||||
}
|
||||
.products-columns-control summary {
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
list-style: none;
|
||||
}
|
||||
.products-columns-control summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.products-columns-control summary::after {
|
||||
content: '\25BC';
|
||||
float: right;
|
||||
font-size: 10px;
|
||||
color: #64748B;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.products-columns-control[open] summary::after {
|
||||
content: '\25B2';
|
||||
}
|
||||
.products-columns-list {
|
||||
border-top: 1px solid #EEF2F7;
|
||||
padding: 8px 10px;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.products-columns-list .products-col-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.products-columns-list .products-col-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.products-columns-list .products-col-item input[type=checkbox] {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
var AI_OPENAI_ENABLED = <?= $openai_enabled ? 'true' : 'false'; ?>;
|
||||
var AI_CLAUDE_ENABLED = <?= $claude_enabled ? 'true' : 'false'; ?>;
|
||||
var PRODUCTS_COLUMNS_STORAGE_KEY = 'products.columns.visibility';
|
||||
var PRODUCTS_LOCKED_COLUMNS = [ 0, 19 ];
|
||||
var PRODUCTS_LOCKED_COLUMNS = [ 0, 20 ];
|
||||
|
||||
function show_toast( message, type )
|
||||
{
|
||||
@@ -160,6 +109,11 @@ function show_toast( message, type )
|
||||
}, 3000 );
|
||||
}
|
||||
|
||||
function escape_html( value )
|
||||
{
|
||||
return $( '<div>' ).text( value == null ? '' : String( value ) ).html();
|
||||
}
|
||||
|
||||
var GOOGLE_TAXONOMY_ENDPOINT = '/tools/google-taxonomy.php';
|
||||
var googleCategories = [];
|
||||
|
||||
@@ -317,7 +271,8 @@ function products_render_columns_picker( table_instance )
|
||||
}
|
||||
|
||||
var id = 'products-col-toggle-' + i;
|
||||
var th = $( '#products thead th' ).eq( i );
|
||||
var header_node = table_instance.column( i ).header();
|
||||
var th = header_node ? $( header_node ) : $();
|
||||
var title = $.trim( th.find( '.dt-column-title' ).first().text() || th.text() ) || ( 'Kolumna ' + i );
|
||||
var checked = table_instance.column( i ).visible() ? ' checked' : '';
|
||||
|
||||
@@ -356,6 +311,7 @@ $( function()
|
||||
{ width: '80px', name: 'offer_id' },
|
||||
{ width: '200px', name: 'campaign_name' },
|
||||
{ width: '200px', name: 'ad_group_name' },
|
||||
{ width: '120px', orderable: false, searchable: false },
|
||||
{ name: 'name' },
|
||||
{ width: '50px', name: 'impressions' },
|
||||
{ width: '80px', name: 'impressions_30' },
|
||||
@@ -370,12 +326,12 @@ $( function()
|
||||
{ width: '70px', name: 'min_roas' },
|
||||
{ width: '50px', name: 'cl3', orderable: false },
|
||||
{ width: '120px', orderable: false },
|
||||
{ width: '50px', orderable: false, className: 'dt-center' }
|
||||
{ width: '190px', orderable: false, className: 'dt-center' }
|
||||
],
|
||||
order: [ [ 8, 'desc' ] ],
|
||||
order: [ [ 9, 'desc' ] ],
|
||||
language: {
|
||||
processing: '£adowanie...',
|
||||
emptyTable: 'Brak produktów do wyœwietlenia',
|
||||
processing: 'Ładowanie...',
|
||||
emptyTable: 'Brak produktów do wyświetlenia',
|
||||
info: 'Produkty _START_ - _END_ z _TOTAL_',
|
||||
infoEmpty: '',
|
||||
paginate: {
|
||||
@@ -466,6 +422,308 @@ $( function()
|
||||
} );
|
||||
}
|
||||
|
||||
$( 'body' ).on( 'click', '.assign-product-scope', function( e )
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
var product_id = $( this ).attr( 'product_id' );
|
||||
var client_id = $( '#client_id' ).val() || '';
|
||||
|
||||
if ( !client_id )
|
||||
{
|
||||
$.alert({ title: 'Brak klienta', content: 'Najpierw wybierz klienta, aby przypisać produkt do kampanii i grupy reklam.', type: 'orange' });
|
||||
return;
|
||||
}
|
||||
|
||||
$.confirm({
|
||||
title: 'Dodaj produkt do kampanii/grupy',
|
||||
content: '' +
|
||||
'<form class="assign-product-form">' +
|
||||
'<div class="assign-step assign-step-1">' +
|
||||
'<h4 style="margin-top:0">Krok 1 z 2: Kampania</h4>' +
|
||||
'<div class="form-group">' +
|
||||
'<label style="display:block"><input type="radio" name="campaign_mode" value="existing" checked> Istniejąca kampania</label>' +
|
||||
'<select class="form-control assign-campaign-id" style="margin-top:8px"><option value="">— wybierz kampanię —</option></select>' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label style="display:block"><input type="radio" name="campaign_mode" value="new"> Nowa kampania</label>' +
|
||||
'<input type="text" class="form-control assign-campaign-name" placeholder="Nazwa nowej kampanii" style="margin-top:8px;display:none">' +
|
||||
'<div class="assign-new-campaign-options" style="display:none;margin-top:8px">' +
|
||||
'<div style="display:flex;gap:8px">' +
|
||||
'<input type="number" min="1" step="0.01" class="form-control assign-campaign-budget" value="50.00" placeholder="Budżet dzienny (np. 50.00 PLN)">' +
|
||||
'<input type="number" min="0.1" step="0.01" class="form-control assign-default-cpc" value="1.00" placeholder="Domyślne CPC (np. 1.00 PLN)">' +
|
||||
'</div>' +
|
||||
'<small class="text-muted">Dotyczy tylko tworzenia nowej kampanii Standard Shopping.</small>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="assign-step assign-step-2" style="display:none">' +
|
||||
'<h4 style="margin-top:0">Krok 2 z 2: Grupa reklam</h4>' +
|
||||
'<div class="form-group">' +
|
||||
'<label style="display:block"><input type="radio" name="ad_group_mode" value="existing" checked> Istniejąca grupa reklam</label>' +
|
||||
'<select class="form-control assign-ad-group-id" style="margin-top:8px"><option value="">— wybierz grupę reklam —</option></select>' +
|
||||
'</div>' +
|
||||
'<div class="form-group">' +
|
||||
'<label style="display:block"><input type="radio" name="ad_group_mode" value="new"> Nowa grupa reklam</label>' +
|
||||
'<input type="text" class="form-control assign-ad-group-name" placeholder="Nazwa nowej grupy reklam" style="margin-top:8px;display:none">' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
useBootstrap: false,
|
||||
boxWidth: '720px',
|
||||
theme: 'modern',
|
||||
buttons: {
|
||||
back: {
|
||||
text: 'Wstecz',
|
||||
isHidden: true,
|
||||
action: function() {
|
||||
this.$content.find( '.assign-step-1' ).show();
|
||||
this.$content.find( '.assign-step-2' ).hide();
|
||||
this.$$back.hide();
|
||||
this.$$next.show();
|
||||
this.$$save.hide();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
next: {
|
||||
text: 'Dalej',
|
||||
btnClass: 'btn-blue',
|
||||
action: function() {
|
||||
var $content = this.$content;
|
||||
var campaign_mode = $content.find( 'input[name="campaign_mode"]:checked' ).val();
|
||||
var campaign_id = $content.find( '.assign-campaign-id' ).val();
|
||||
var campaign_name = $.trim( $content.find( '.assign-campaign-name' ).val() );
|
||||
var campaign_daily_budget = parseFloat( $content.find( '.assign-campaign-budget' ).val() || '0' );
|
||||
var default_cpc = parseFloat( $content.find( '.assign-default-cpc' ).val() || '0' );
|
||||
|
||||
if ( campaign_mode === 'existing' && !campaign_id )
|
||||
{
|
||||
$.alert( 'Wybierz kampanię.' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( campaign_mode === 'new' && !campaign_name )
|
||||
{
|
||||
$.alert( 'Podaj nazwę nowej kampanii.' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( campaign_mode === 'new' && ( isNaN( campaign_daily_budget ) || campaign_daily_budget <= 0 ) )
|
||||
{
|
||||
$.alert( 'Podaj poprawny budżet dzienny (większy od 0).' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( campaign_mode === 'new' && ( isNaN( default_cpc ) || default_cpc <= 0 ) )
|
||||
{
|
||||
$.alert( 'Podaj poprawne domyślne CPC (większe od 0).' );
|
||||
return false;
|
||||
}
|
||||
|
||||
this.$content.find( '.assign-step-1' ).hide();
|
||||
this.$content.find( '.assign-step-2' ).show();
|
||||
this.$$back.show();
|
||||
this.$$next.hide();
|
||||
this.$$save.show();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
save: {
|
||||
text: 'Zapisz',
|
||||
btnClass: 'btn-green',
|
||||
isHidden: true,
|
||||
action: function() {
|
||||
var jc = this;
|
||||
var $content = jc.$content;
|
||||
|
||||
var campaign_mode = $content.find( 'input[name="campaign_mode"]:checked' ).val();
|
||||
var campaign_id = $content.find( '.assign-campaign-id' ).val() || '';
|
||||
var campaign_name = $.trim( $content.find( '.assign-campaign-name' ).val() );
|
||||
var campaign_daily_budget = parseFloat( $content.find( '.assign-campaign-budget' ).val() || '0' );
|
||||
var default_cpc = parseFloat( $content.find( '.assign-default-cpc' ).val() || '0' );
|
||||
|
||||
var ad_group_mode = $content.find( 'input[name="ad_group_mode"]:checked' ).val();
|
||||
var ad_group_id = $content.find( '.assign-ad-group-id' ).val() || '';
|
||||
var ad_group_name = $.trim( $content.find( '.assign-ad-group-name' ).val() );
|
||||
|
||||
if ( campaign_mode === 'existing' && !campaign_id )
|
||||
{
|
||||
$.alert( 'Wybierz kampanię.' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( campaign_mode === 'new' && !campaign_name )
|
||||
{
|
||||
$.alert( 'Podaj nazwę nowej kampanii.' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( campaign_mode === 'new' && ( isNaN( campaign_daily_budget ) || campaign_daily_budget <= 0 ) )
|
||||
{
|
||||
$.alert( 'Podaj poprawny budżet dzienny (większy od 0).' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( campaign_mode === 'new' && ( isNaN( default_cpc ) || default_cpc <= 0 ) )
|
||||
{
|
||||
$.alert( 'Podaj poprawne domyślne CPC (większe od 0).' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ad_group_mode === 'existing' && !ad_group_id )
|
||||
{
|
||||
$.alert( 'Wybierz grupę reklam.' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ad_group_mode === 'new' && !ad_group_name )
|
||||
{
|
||||
$.alert( 'Podaj nazwę nowej grupy reklam.' );
|
||||
return false;
|
||||
}
|
||||
|
||||
jc.showLoading( true );
|
||||
|
||||
$.ajax({
|
||||
url: '/products/assign_product_scope/',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
product_id: product_id,
|
||||
campaign_mode: campaign_mode,
|
||||
campaign_id: campaign_id,
|
||||
campaign_name: campaign_name,
|
||||
campaign_daily_budget: campaign_daily_budget,
|
||||
default_cpc: default_cpc,
|
||||
ad_group_mode: ad_group_mode,
|
||||
ad_group_id: ad_group_id,
|
||||
ad_group_name: ad_group_name
|
||||
},
|
||||
success: function( res ) {
|
||||
jc.hideLoading();
|
||||
|
||||
if ( res && res.status === 'ok' )
|
||||
{
|
||||
jc.close();
|
||||
reload_products_table();
|
||||
show_toast( 'Produkt został przypisany do kampanii i grupy reklam.', 'success' );
|
||||
}
|
||||
else
|
||||
{
|
||||
show_toast( ( res && res.message ) ? res.message : 'Nie udało się zapisać przypisania.', 'error' );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
jc.hideLoading();
|
||||
show_toast( 'Błąd połączenia podczas przypisywania produktu.', 'error' );
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
text: 'Anuluj'
|
||||
}
|
||||
},
|
||||
onContentReady: function() {
|
||||
var jc = this;
|
||||
var $content = jc.$content;
|
||||
var $campaignModeInputs = $content.find( 'input[name="campaign_mode"]' );
|
||||
var $campaignSelect = $content.find( '.assign-campaign-id' );
|
||||
var $campaignName = $content.find( '.assign-campaign-name' );
|
||||
var $newCampaignOptions = $content.find( '.assign-new-campaign-options' );
|
||||
|
||||
var $adGroupModeInputs = $content.find( 'input[name="ad_group_mode"]' );
|
||||
var $adGroupSelect = $content.find( '.assign-ad-group-id' );
|
||||
var $adGroupName = $content.find( '.assign-ad-group-name' );
|
||||
|
||||
function loadCampaignsForStep()
|
||||
{
|
||||
$campaignSelect.empty().append( '<option value="">— wybierz kampanię —</option>' );
|
||||
|
||||
return $.ajax({
|
||||
url: '/products/get_campaigns_list/client_id=' + client_id,
|
||||
type: 'GET',
|
||||
dataType: 'json'
|
||||
}).done( function( res ) {
|
||||
( res.campaigns || [] ).forEach( function( row ) {
|
||||
$campaignSelect.append( '<option value="' + row.id + '">' + escape_html( row.campaign_name || '' ) + '</option>' );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
|
||||
function loadAdGroupsForStep( campaign_id )
|
||||
{
|
||||
$adGroupSelect.empty().append( '<option value="">— wybierz grupę reklam —</option>' );
|
||||
|
||||
if ( !campaign_id )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/products/get_campaign_ad_groups/campaign_id=' + campaign_id,
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
success: function( res ) {
|
||||
( res.ad_groups || [] ).forEach( function( row ) {
|
||||
$adGroupSelect.append( '<option value="' + row.id + '">' + escape_html( row.ad_group_name || '' ) + '</option>' );
|
||||
} );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleCampaignMode()
|
||||
{
|
||||
var mode = $campaignModeInputs.filter( ':checked' ).val();
|
||||
var isExisting = mode === 'existing';
|
||||
|
||||
$campaignSelect.toggle( isExisting );
|
||||
$campaignName.toggle( !isExisting );
|
||||
$newCampaignOptions.toggle( !isExisting );
|
||||
|
||||
if ( isExisting )
|
||||
{
|
||||
$adGroupModeInputs.filter( '[value="existing"]' ).prop( 'disabled', false );
|
||||
loadAdGroupsForStep( $campaignSelect.val() || '' );
|
||||
}
|
||||
else
|
||||
{
|
||||
$adGroupModeInputs.filter( '[value="existing"]' ).prop( 'disabled', true );
|
||||
$adGroupModeInputs.filter( '[value="new"]' ).prop( 'checked', true );
|
||||
$adGroupSelect.empty().append( '<option value="">— wybierz grupę reklam —</option>' );
|
||||
}
|
||||
|
||||
toggleAdGroupMode();
|
||||
}
|
||||
|
||||
function toggleAdGroupMode()
|
||||
{
|
||||
var mode = $adGroupModeInputs.filter( ':checked' ).val();
|
||||
var isExisting = mode === 'existing';
|
||||
|
||||
$adGroupSelect.toggle( isExisting );
|
||||
$adGroupName.toggle( !isExisting );
|
||||
}
|
||||
|
||||
$campaignModeInputs.on( 'change', toggleCampaignMode );
|
||||
$adGroupModeInputs.on( 'change', toggleAdGroupMode );
|
||||
|
||||
$campaignSelect.on( 'change', function() {
|
||||
if ( $campaignModeInputs.filter( ':checked' ).val() === 'existing' )
|
||||
{
|
||||
loadAdGroupsForStep( $( this ).val() || '' );
|
||||
}
|
||||
} );
|
||||
|
||||
loadCampaignsForStep().always( function() {
|
||||
toggleCampaignMode();
|
||||
} );
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'change', '#client_id', function()
|
||||
{
|
||||
var client_id = $( this ).val() || '';
|
||||
@@ -516,6 +774,102 @@ $( function()
|
||||
} );
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'click', '.view-merchant-logs', function( e )
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
var product_id = $( this ).attr( 'product_id' );
|
||||
|
||||
$.confirm({
|
||||
title: 'Logi synchronizacji Merchant (produkt #' + product_id + ')',
|
||||
content: '<div class="merchant-logs-wrap" style="max-height:460px;overflow:auto">Ładowanie logów...</div>',
|
||||
useBootstrap: false,
|
||||
boxWidth: '1100px',
|
||||
theme: 'modern',
|
||||
buttons: {
|
||||
close: {
|
||||
text: 'Zamknij',
|
||||
btnClass: 'btn-blue'
|
||||
}
|
||||
},
|
||||
onContentReady: function()
|
||||
{
|
||||
var jc = this;
|
||||
var $wrap = jc.$content.find( '.merchant-logs-wrap' );
|
||||
|
||||
$.ajax({
|
||||
url: '/products/get_product_merchant_sync_logs/',
|
||||
type: 'POST',
|
||||
data: { product_id: product_id, limit: 100 },
|
||||
success: function( response )
|
||||
{
|
||||
var data;
|
||||
|
||||
try
|
||||
{
|
||||
data = JSON.parse( response );
|
||||
}
|
||||
catch ( err )
|
||||
{
|
||||
$wrap.html( '<div class="text-danger">Nie udało się odczytać odpowiedzi serwera.</div>' );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( data.status !== 'ok' )
|
||||
{
|
||||
$wrap.html( '<div class="text-danger">' + escape_html( data.message || 'Błąd pobierania logów.' ) + '</div>' );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !data.logs || !data.logs.length )
|
||||
{
|
||||
$wrap.html( '<div class="text-muted">Brak logów synchronizacji dla tego produktu.</div>' );
|
||||
return;
|
||||
}
|
||||
|
||||
var rows_html = '';
|
||||
$.each( data.logs, function( _, log ) {
|
||||
var status_class = log.sync_status === 'success'
|
||||
? 'text-success'
|
||||
: ( log.sync_status === 'error' ? 'text-danger' : 'text-muted' );
|
||||
|
||||
rows_html += '<tr>' +
|
||||
'<td>' + escape_html( log.date_add || '' ) + '</td>' +
|
||||
'<td>' + escape_html( log.field_name || '' ) + '</td>' +
|
||||
'<td class="' + status_class + '"><b>' + escape_html( log.sync_status || '' ) + '</b></td>' +
|
||||
'<td>' + escape_html( log.sync_source || '' ) + '</td>' +
|
||||
'<td>' + escape_html( log.old_value || '' ) + '</td>' +
|
||||
'<td>' + escape_html( log.new_value || '' ) + '</td>' +
|
||||
'<td>' + escape_html( log.error_message || '' ) + '</td>' +
|
||||
'</tr>';
|
||||
} );
|
||||
|
||||
$wrap.html(
|
||||
'<table class="table table-sm table-bordered table-striped" style="font-size:12px;">' +
|
||||
'<thead>' +
|
||||
'<tr>' +
|
||||
'<th style="min-width:140px;">Data</th>' +
|
||||
'<th style="min-width:120px;">Pole</th>' +
|
||||
'<th style="min-width:90px;">Status</th>' +
|
||||
'<th style="min-width:110px;">Źródło</th>' +
|
||||
'<th style="min-width:180px;">Stara wartość</th>' +
|
||||
'<th style="min-width:180px;">Nowa wartość</th>' +
|
||||
'<th style="min-width:220px;">Błąd</th>' +
|
||||
'</tr>' +
|
||||
'</thead>' +
|
||||
'<tbody>' + rows_html + '</tbody>' +
|
||||
'</table>'
|
||||
);
|
||||
},
|
||||
error: function()
|
||||
{
|
||||
$wrap.html( '<div class="text-danger">Nie udało się pobrać logów synchronizacji.</div>' );
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Usuwanie produktu
|
||||
$( 'body' ).on( 'click', '.delete-product', function( e )
|
||||
{
|
||||
@@ -584,7 +938,34 @@ $( function()
|
||||
$.ajax({
|
||||
url: '/products/save_custom_label_4/',
|
||||
type: 'POST',
|
||||
data: { product_id: product_id, custom_label_4: custom_label_4 }
|
||||
data: { product_id: product_id, custom_label_4: custom_label_4 },
|
||||
success: function( response )
|
||||
{
|
||||
var data;
|
||||
|
||||
try
|
||||
{
|
||||
data = JSON.parse( response );
|
||||
}
|
||||
catch ( e )
|
||||
{
|
||||
show_toast( 'Custom Label 4: nieprawidłowa odpowiedź serwera.', 'error' );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( data.status === 'ok' )
|
||||
{
|
||||
show_toast( 'Custom Label 4 zapisany.', 'success' );
|
||||
}
|
||||
else
|
||||
{
|
||||
show_toast( 'Custom Label 4: zapis nie powiódł się.', 'error' );
|
||||
}
|
||||
},
|
||||
error: function()
|
||||
{
|
||||
show_toast( 'Custom Label 4: błąd połączenia podczas zapisu.', 'error' );
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -673,6 +1054,7 @@ $( function()
|
||||
jc.hideLoading();
|
||||
if ( data.status == 'ok' ) {
|
||||
jc.close();
|
||||
reload_products_table();
|
||||
show_toast( 'Dane produktu zostały zapisane.', 'success' );
|
||||
} else {
|
||||
show_toast( 'Błąd: ' + response, 'error' );
|
||||
@@ -789,10 +1171,18 @@ $( function()
|
||||
}
|
||||
if ( data.warning ) {
|
||||
show_toast( providerLabel + ': ' + data.warning, 'error' );
|
||||
} else if ( data.page_fetched ) {
|
||||
show_toast( providerLabel + ': Sugestia wygenerowana z treścią strony produktu', 'success' );
|
||||
} else {
|
||||
show_toast( providerLabel + ': Sugestia wygenerowana', 'success' );
|
||||
var successMessage = data.page_fetched
|
||||
? providerLabel + ': Sugestia wygenerowana z treścią strony produktu'
|
||||
: providerLabel + ': Sugestia wygenerowana';
|
||||
|
||||
if ( ( field == 'title' || field == 'description' ) && data.keyword_planner_terms_used ) {
|
||||
var kwCount = parseInt( data.keyword_planner_terms_count || 0, 10 );
|
||||
var kwLabel = kwCount > 0 ? ' (' + kwCount + ' fraz)' : '';
|
||||
successMessage += '; użyto fraz z Keyword Planner' + kwLabel;
|
||||
}
|
||||
|
||||
show_toast( successMessage, 'success' );
|
||||
}
|
||||
} else {
|
||||
show_toast( providerLabel + ': ' + ( data.message || 'Wystąpił błąd AI.' ), 'error' );
|
||||
|
||||
@@ -1,56 +1,60 @@
|
||||
<div class="admin-form theme-primary">
|
||||
<div class="panel heading-border panel-primary">
|
||||
<div class="panel-body">
|
||||
<div class="chart-with-form">
|
||||
<div class="chart-area">
|
||||
<figure class="highcharts-figure">
|
||||
<div id="container"></div>
|
||||
</figure>
|
||||
</div>
|
||||
<div class="products-page product-history-page">
|
||||
<div class="products-header">
|
||||
<h2><i class="fa-solid fa-chart-line"></i> Historia produktu #<?= (int) $this -> product_id; ?></h2>
|
||||
<a href="/products" class="btn btn-primary btn-sm">
|
||||
<i class="fa-solid fa-arrow-left"></i> Powrot do produktow
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- PRAWY PANEL: formularz komentarza -->
|
||||
<aside class="comment-form admin-form theme-primary">
|
||||
<form id="product-comment-form" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label for="comment_date">Data</label>
|
||||
<input type="date" id="comment_date" name="date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="comment_text">Komentarz</label>
|
||||
<textarea id="comment_text" name="comment" placeholder="Wpisz komentarz..." required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn" id="save_comment">Zapisz komentarz</button>
|
||||
</form>
|
||||
</aside>
|
||||
<div class="product-history-meta">
|
||||
<span>Klient: #<?= (int) $this -> client_id; ?></span>
|
||||
<span>Kampania: <?= (int) ( $this -> campaign_id ?? 0 ); ?></span>
|
||||
<span>Grupa reklam: <?= (int) ( $this -> ad_group_id ?? 0 ); ?></span>
|
||||
</div>
|
||||
|
||||
<div class="product-history-chart-wrap">
|
||||
<div class="chart-with-form">
|
||||
<div class="chart-area">
|
||||
<div class="product-history-chart" id="container"></div>
|
||||
</div>
|
||||
<aside class="comment-form">
|
||||
<form id="product-comment-form" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label for="comment_date">Data</label>
|
||||
<input type="date" id="comment_date" name="date" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="comment_text">Komentarz</label>
|
||||
<textarea id="comment_text" name="comment" placeholder="Wpisz komentarz..." required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm" id="save_comment">
|
||||
<i class="fa-solid fa-floppy-disk"></i> Zapisz komentarz
|
||||
</button>
|
||||
</form>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form theme-primary">
|
||||
<div class="panel heading-border panel-primary">
|
||||
<div class="panel-body">
|
||||
<table class="table" id="products">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Id</th>
|
||||
<th scope="col">Wyświetlenia</th>
|
||||
<th scope="col">Kliknięcia</th>
|
||||
<th scope="col">CTR</th>
|
||||
<th scope="col">Koszt</th>
|
||||
<th scope="col">Konwersje</th>
|
||||
<th scope="col">Wartość konwersji</th>
|
||||
<th scope="col">ROAS</th>
|
||||
<th scope="col">Data</th>
|
||||
<th scope="col">Komentarz</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="products-table-wrap">
|
||||
<table class="table table-sm" id="products">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Id</th>
|
||||
<th scope="col">Wyswietlenia</th>
|
||||
<th scope="col">Klikniecia</th>
|
||||
<th scope="col">CTR</th>
|
||||
<th scope="col">Koszt</th>
|
||||
<th scope="col">Konwersje</th>
|
||||
<th scope="col">Wartosc konwersji</th>
|
||||
<th scope="col">ROAS</th>
|
||||
<th scope="col">Data</th>
|
||||
<th scope="col">Komentarz</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.highcharts.com/highcharts.js"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
@@ -71,14 +75,13 @@
|
||||
})();
|
||||
|
||||
// Inicjalizacja tabeli
|
||||
var table = $('#products').DataTable();
|
||||
table.destroy();
|
||||
|
||||
new DataTable('#products', {
|
||||
var table = new DataTable('#products', {
|
||||
ajax: {
|
||||
type: 'POST',
|
||||
url: '/products/get_product_history_table/client_id=' + client_id + '&product_id=' + product_id + '&campaign_id=' + campaign_id + '&ad_group_id=' + ad_group_id,
|
||||
},
|
||||
autoWidth: false,
|
||||
lengthChange: false,
|
||||
pageLength: 30,
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
@@ -95,7 +98,19 @@
|
||||
{ width: '100px', name: 'date_add' },
|
||||
{ width: '250px', name: 'comment', orderable: false },
|
||||
],
|
||||
order: [[0, false],[8, 'desc']]
|
||||
order: [ [ 8, 'desc' ] ],
|
||||
language: {
|
||||
processing: 'Ladowanie...',
|
||||
emptyTable: 'Brak danych do wyswietlenia',
|
||||
info: 'Wiersze _START_ - _END_ z _TOTAL_',
|
||||
infoEmpty: '',
|
||||
paginate: {
|
||||
first: 'Pierwsza',
|
||||
last: 'Ostatnia',
|
||||
next: 'Dalej',
|
||||
previous: 'Wstecz'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// WYKRES
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
<body class="logged">
|
||||
<?php
|
||||
$module = $this -> current_module;
|
||||
$google_ads_modules = [ 'campaigns', 'campaign_terms', 'products', 'clients', 'xml_files' ];
|
||||
$is_google_ads_module = in_array( $module, $google_ads_modules, true );
|
||||
?>
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
@@ -52,29 +54,43 @@
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<ul>
|
||||
<li class="<?= $module === 'campaigns' ? 'active' : '' ?>">
|
||||
<a href="/campaigns">
|
||||
<i class="fa-solid fa-bullhorn"></i>
|
||||
<span>Kampanie</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<?= $module === 'campaign_terms' ? 'active' : '' ?>">
|
||||
<a href="/campaign_terms">
|
||||
<i class="fa-solid fa-list-check"></i>
|
||||
<span>Grupy i frazy</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<?= $module === 'products' ? 'active' : '' ?>">
|
||||
<a href="/products">
|
||||
<i class="fa-solid fa-box-open"></i>
|
||||
<span>Produkty</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<?= $module === 'clients' ? 'active' : '' ?>">
|
||||
<a href="/clients">
|
||||
<i class="fa-solid fa-building"></i>
|
||||
<span>Klienci</span>
|
||||
</a>
|
||||
<li class="nav-group <?= $is_google_ads_module ? 'active' : '' ?>">
|
||||
<div class="nav-group-label">
|
||||
<i class="fa-brands fa-google"></i>
|
||||
<span>Google ADS</span>
|
||||
</div>
|
||||
<ul class="nav-submenu">
|
||||
<li class="<?= $module === 'campaigns' ? 'active' : '' ?>">
|
||||
<a href="/campaigns">
|
||||
<i class="fa-solid fa-bullhorn"></i>
|
||||
<span>Kampanie</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<?= $module === 'campaign_terms' ? 'active' : '' ?>">
|
||||
<a href="/campaign_terms">
|
||||
<i class="fa-solid fa-list-check"></i>
|
||||
<span>Grupy i frazy</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<?= $module === 'products' ? 'active' : '' ?>">
|
||||
<a href="/products">
|
||||
<i class="fa-solid fa-box-open"></i>
|
||||
<span>Produkty</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<?= $module === 'clients' ? 'active' : '' ?>">
|
||||
<a href="/clients">
|
||||
<i class="fa-solid fa-building"></i>
|
||||
<span>Klienci</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<?= $module === 'xml_files' ? 'active' : '' ?>">
|
||||
<a href="/xml_files">
|
||||
<i class="fa-solid fa-file-code"></i>
|
||||
<span>Pliki XML</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="<?= $module === 'allegro' ? 'active' : '' ?>">
|
||||
<a href="/allegro">
|
||||
@@ -120,6 +136,7 @@
|
||||
'campaign_terms' => 'Grupy i frazy',
|
||||
'products' => 'Produkty',
|
||||
'clients' => 'Klienci',
|
||||
'xml_files' => 'Pliki XML',
|
||||
'allegro' => 'Allegro import',
|
||||
'users' => 'Ustawienia',
|
||||
];
|
||||
|
||||
@@ -1,197 +1,292 @@
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-icon"><i class="fa-solid fa-lock"></i></div>
|
||||
<div>
|
||||
<h3>Zmiana hasła</h3>
|
||||
<small>Zmień swoje stare hasło na nowe</small>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" id="password-settings" action="/users/password_change/">
|
||||
<div class="settings-field">
|
||||
<label for="password_old">Stare hasło</label>
|
||||
<div class="settings-input-wrap">
|
||||
<i class="fa-solid fa-key settings-input-icon"></i>
|
||||
<input type="password" id="password_old" name="password_old" class="form-control" required placeholder="Wprowadź stare hasło" />
|
||||
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'password_old' )">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="password_new">Nowe hasło</label>
|
||||
<div class="settings-input-wrap">
|
||||
<i class="fa-solid fa-key settings-input-icon"></i>
|
||||
<input type="password" id="password_new" name="password_new" class="form-control" required placeholder="Wprowadź nowe hasło" />
|
||||
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'password_new' )">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success"><i class="fa-solid fa-check mr5"></i>Zmień hasło</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$settings_tab = \S::get( 'settings_tab' ) === 'cron' ? 'cron' : 'general';
|
||||
$cron_data = is_array( $this -> cron_data ?? null ) ? $this -> cron_data : [];
|
||||
$cron_progress = is_array( $cron_data['progress'] ?? null ) ? $cron_data['progress'] : [];
|
||||
$cron_urls = is_array( $cron_data['urls'] ?? null ) ? $cron_data['urls'] : [];
|
||||
?>
|
||||
|
||||
<div class="settings-tabs">
|
||||
<a href="/settings?settings_tab=general" class="settings-tab <?= $settings_tab === 'general' ? 'active' : ''; ?>">
|
||||
<i class="fa-solid fa-sliders"></i> Ustawienia
|
||||
</a>
|
||||
<a href="/settings?settings_tab=cron" class="settings-tab <?= $settings_tab === 'cron' ? 'active' : ''; ?>">
|
||||
<i class="fa-solid fa-clock-rotate-left"></i> CRON
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 25px;">
|
||||
<div class="col-12">
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-icon"><i class="fa-brands fa-google"></i></div>
|
||||
<div>
|
||||
<h3>Google Ads API</h3>
|
||||
<small>Dane do połączenia z Google Ads REST API (wymagane do synchronizacji kampanii)</small>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$last_error = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
|
||||
if ( $last_error ): ?>
|
||||
<div class="settings-alert-error">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||
<span><strong>Ostatni błąd API:</strong> <?= htmlspecialchars( $last_error ); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form method="POST" id="google-ads-settings" action="/settings/save_google_ads">
|
||||
<div class="settings-fields-grid">
|
||||
<div class="settings-field">
|
||||
<label for="google_ads_developer_token">Developer Token</label>
|
||||
<input type="text" id="google_ads_developer_token" name="google_ads_developer_token" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'google_ads_developer_token' ) ); ?>" placeholder="np. ABcdEf1234..." />
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="google_ads_client_id">OAuth2 Client ID</label>
|
||||
<input type="text" id="google_ads_client_id" name="google_ads_client_id" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'google_ads_client_id' ) ); ?>" placeholder="np. 123456789.apps.googleusercontent.com" />
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="google_ads_client_secret">OAuth2 Client Secret</label>
|
||||
<div class="settings-input-wrap">
|
||||
<input type="password" id="google_ads_client_secret" name="google_ads_client_secret" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'google_ads_client_secret' ) ); ?>" />
|
||||
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'google_ads_client_secret' )">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="google_ads_refresh_token">OAuth2 Refresh Token</label>
|
||||
<div class="settings-input-wrap">
|
||||
<input type="password" id="google_ads_refresh_token" name="google_ads_refresh_token" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'google_ads_refresh_token' ) ); ?>" />
|
||||
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'google_ads_refresh_token' )">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="google_ads_manager_account_id">Manager Account ID <span class="text-muted">(opcjonalnie, dla MCC)</span></label>
|
||||
<input type="text" id="google_ads_manager_account_id" name="google_ads_manager_account_id" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'google_ads_manager_account_id' ) ); ?>" placeholder="np. 123-456-7890" />
|
||||
<?php if ( $settings_tab === 'cron' ): ?>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-icon"><i class="fa-solid fa-clock-rotate-left"></i></div>
|
||||
<div>
|
||||
<h3>Status CRON</h3>
|
||||
<small>Adresy URL do wywołań, postęp pipeline i ostatnie uruchomienia</small>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success" onclick="$( '#google-ads-settings' ).submit();"><i class="fa-solid fa-check mr5"></i>Zapisz ustawienia Google Ads</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 25px;">
|
||||
<div class="col-12">
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-icon"><i class="fa-solid fa-robot"></i></div>
|
||||
<div>
|
||||
<h3>OpenAI (ChatGPT)</h3>
|
||||
<small>Klucz API i model do optymalizacji tytułów i opisów produktów przez AI</small>
|
||||
<div class="cron-status-overview">
|
||||
<div><strong>Ostatnie wywołanie:</strong> <span data-cron-overall-last-invoked><?= htmlspecialchars( (string) ( $cron_data['overall_last_invoked_at'] ?? 'Brak danych' ) ); ?></span></div>
|
||||
<div><strong>Klienci z Google Ads ID:</strong> <span data-cron-clients-total><?= (int) ( $cron_data['clients_total'] ?? 0 ); ?></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" id="openai-settings" action="/settings/save_openai">
|
||||
<div class="settings-field" style="margin-bottom: 16px;">
|
||||
<label class="settings-toggle-label">
|
||||
<?php $openai_enabled = \services\GoogleAdsApi::get_setting( 'openai_enabled' ) !== '0'; ?>
|
||||
<input type="hidden" name="openai_enabled" value="0" />
|
||||
<input type="checkbox" name="openai_enabled" value="1" class="settings-toggle-checkbox" <?= $openai_enabled ? 'checked' : ''; ?> />
|
||||
<span class="settings-toggle-switch"></span>
|
||||
<span>Włącz OpenAI (ChatGPT)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-fields-grid">
|
||||
<div class="settings-field">
|
||||
<label for="openai_api_key">API Key</label>
|
||||
<div class="settings-input-wrap">
|
||||
<input type="password" id="openai_api_key" name="openai_api_key" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'openai_api_key' ) ); ?>" placeholder="sk-..." />
|
||||
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'openai_api_key' )">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="openai_model">Model</label>
|
||||
<?php $current_model = \services\GoogleAdsApi::get_setting( 'openai_model' ) ?: 'gpt-5-mini'; ?>
|
||||
<select id="openai_model" name="openai_model" class="form-control">
|
||||
<option value="gpt-5.2" <?= $current_model === 'gpt-5.2' ? 'selected' : ''; ?>>GPT-5.2 (najnowszy, $1.75/$14 per 1M)</option>
|
||||
<option value="gpt-5-mini" <?= $current_model === 'gpt-5-mini' ? 'selected' : ''; ?>>GPT-5 Mini (szybki, $0.25/$2 per 1M)</option>
|
||||
<option value="gpt-4.1" <?= $current_model === 'gpt-4.1' ? 'selected' : ''; ?>>GPT-4.1</option>
|
||||
<option value="gpt-4.1-mini" <?= $current_model === 'gpt-4.1-mini' ? 'selected' : ''; ?>>GPT-4.1 Mini</option>
|
||||
<option value="gpt-4o" <?= $current_model === 'gpt-4o' ? 'selected' : ''; ?>>GPT-4o (legacy)</option>
|
||||
<option value="gpt-4o-mini" <?= $current_model === 'gpt-4o-mini' ? 'selected' : ''; ?>>GPT-4o Mini (legacy)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success" onclick="$( '#openai-settings' ).submit();"><i class="fa-solid fa-check mr5"></i>Zapisz ustawienia OpenAI</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 25px;">
|
||||
<div class="col-12">
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-icon"><i class="fa-solid fa-brain"></i></div>
|
||||
<div>
|
||||
<h3>Claude (Anthropic)</h3>
|
||||
<small>Klucz API i model Claude do optymalizacji tytułów i opisów produktów przez AI</small>
|
||||
<div class="cron-progress-list">
|
||||
<?php foreach ( $cron_progress as $progress_index => $item ): ?>
|
||||
<?php
|
||||
$processed = (int) ( $item['processed'] ?? 0 );
|
||||
$total = (int) ( $item['total'] ?? 0 );
|
||||
$percent = (int) ( $item['percent'] ?? 0 );
|
||||
?>
|
||||
<div class="cron-progress-item" data-cron-progress-item="<?= (int) $progress_index; ?>">
|
||||
<div class="cron-progress-head">
|
||||
<strong><?= htmlspecialchars( (string) ( $item['name'] ?? 'CRON' ) ); ?></strong>
|
||||
<span data-cron-progress-summary><?= $processed; ?> / <?= $total; ?> (<?= $percent; ?>%)</span>
|
||||
</div>
|
||||
<div class="cron-progress-bar">
|
||||
<span data-cron-progress-fill style="width: <?= $percent; ?>%;"></span>
|
||||
</div>
|
||||
<small data-cron-progress-meta><?= htmlspecialchars( (string) ( $item['meta'] ?? '' ) ); ?></small>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="cron-url-list">
|
||||
<?php foreach ( $cron_urls as $url_index => $row ): ?>
|
||||
<div class="cron-url-item" data-cron-url-item="<?= (int) $url_index; ?>">
|
||||
<div class="cron-url-top">
|
||||
<strong><?= htmlspecialchars( (string) ( $row['name'] ?? 'Cron' ) ); ?></strong>
|
||||
<small>Ostatnio: <span data-cron-url-last-invoked><?= htmlspecialchars( (string) ( $row['last_invoked_at'] ?? 'Brak danych' ) ); ?></span></small>
|
||||
</div>
|
||||
<code><?= htmlspecialchars( (string) ( $row['url'] ?? '' ) ); ?></code>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<a href="/settings?settings_tab=cron" class="btn btn-primary btn-sm">
|
||||
<i class="fa-solid fa-rotate-right mr5"></i> Odśwież status
|
||||
</a>
|
||||
</div>
|
||||
<form method="POST" id="claude-settings" action="/settings/save_claude">
|
||||
<div class="settings-field" style="margin-bottom: 16px;">
|
||||
<label class="settings-toggle-label">
|
||||
<?php $claude_enabled = \services\GoogleAdsApi::get_setting( 'claude_enabled' ) !== '0'; ?>
|
||||
<input type="hidden" name="claude_enabled" value="0" />
|
||||
<input type="checkbox" name="claude_enabled" value="1" class="settings-toggle-checkbox" <?= $claude_enabled ? 'checked' : ''; ?> />
|
||||
<span class="settings-toggle-switch"></span>
|
||||
<span>Włącz Claude (Anthropic)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-icon"><i class="fa-solid fa-lock"></i></div>
|
||||
<div>
|
||||
<h3>Zmiana hasła</h3>
|
||||
<small>Zmień swoje stare hasło na nowe</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-fields-grid">
|
||||
<form method="POST" id="password-settings" action="/users/password_change/">
|
||||
<div class="settings-field">
|
||||
<label for="claude_api_key">API Key</label>
|
||||
<label for="password_old">Stare hasło</label>
|
||||
<div class="settings-input-wrap">
|
||||
<input type="password" id="claude_api_key" name="claude_api_key" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'claude_api_key' ) ); ?>" placeholder="sk-ant-..." />
|
||||
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'claude_api_key' )">
|
||||
<i class="fa-solid fa-key settings-input-icon"></i>
|
||||
<input type="password" id="password_old" name="password_old" class="form-control" required placeholder="Wprowadź stare hasło" />
|
||||
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'password_old' )">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="claude_model">Model</label>
|
||||
<?php $current_claude_model = \services\GoogleAdsApi::get_setting( 'claude_model' ) ?: 'claude-sonnet-4-5-20250929'; ?>
|
||||
<select id="claude_model" name="claude_model" class="form-control">
|
||||
<option value="claude-opus-4-6" <?= $current_claude_model === 'claude-opus-4-6' ? 'selected' : ''; ?>>Claude Opus 4.6 (najpotężniejszy)</option>
|
||||
<option value="claude-sonnet-4-5-20250929" <?= $current_claude_model === 'claude-sonnet-4-5-20250929' ? 'selected' : ''; ?>>Claude Sonnet 4.5 (zbalansowany)</option>
|
||||
<option value="claude-haiku-4-5-20251001" <?= $current_claude_model === 'claude-haiku-4-5-20251001' ? 'selected' : ''; ?>>Claude Haiku 4.5 (szybki, tani)</option>
|
||||
</select>
|
||||
<label for="password_new">Nowe hasło</label>
|
||||
<div class="settings-input-wrap">
|
||||
<i class="fa-solid fa-key settings-input-icon"></i>
|
||||
<input type="password" id="password_new" name="password_new" class="form-control" required placeholder="Wprowadź nowe hasło" />
|
||||
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'password_new' )">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success" onclick="$( '#claude-settings' ).submit();"><i class="fa-solid fa-check mr5"></i>Zapisz ustawienia Claude</button>
|
||||
</form>
|
||||
<button type="submit" class="btn btn-success"><i class="fa-solid fa-check mr5"></i>Zmień hasło</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 25px;">
|
||||
<div class="col-12">
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-icon"><i class="fa-brands fa-google"></i></div>
|
||||
<div>
|
||||
<h3>Google Ads API</h3>
|
||||
<small>Dane do połączenia z Google Ads REST API (wymagane do synchronizacji kampanii)</small>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$last_error = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
|
||||
if ( $last_error ): ?>
|
||||
<div class="settings-alert-error">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||
<span><strong>Ostatni błąd API:</strong> <?= htmlspecialchars( $last_error ); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form method="POST" id="google-ads-settings" action="/settings/save_google_ads">
|
||||
<div class="settings-field" style="margin-bottom: 16px;">
|
||||
<label class="settings-toggle-label">
|
||||
<?php $google_ads_debug_enabled = \services\GoogleAdsApi::get_setting( 'google_ads_debug_enabled' ) !== '0'; ?>
|
||||
<input type="hidden" name="google_ads_debug_enabled" value="0" />
|
||||
<input type="checkbox" name="google_ads_debug_enabled" value="1" class="settings-toggle-checkbox" <?= $google_ads_debug_enabled ? 'checked' : ''; ?> />
|
||||
<span class="settings-toggle-switch"></span>
|
||||
<span class="settings-toggle-text">Włącz debug Google Ads API</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-fields-grid">
|
||||
<div class="settings-field">
|
||||
<label for="google_ads_developer_token">Developer Token</label>
|
||||
<input type="text" id="google_ads_developer_token" name="google_ads_developer_token" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'google_ads_developer_token' ) ); ?>" placeholder="np. ABcdEf1234..." />
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="google_ads_client_id">OAuth2 Client ID</label>
|
||||
<input type="text" id="google_ads_client_id" name="google_ads_client_id" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'google_ads_client_id' ) ); ?>" placeholder="np. 123456789.apps.googleusercontent.com" />
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="google_ads_client_secret">OAuth2 Client Secret</label>
|
||||
<div class="settings-input-wrap">
|
||||
<input type="password" id="google_ads_client_secret" name="google_ads_client_secret" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'google_ads_client_secret' ) ); ?>" />
|
||||
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'google_ads_client_secret' )">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="google_ads_refresh_token">OAuth2 Refresh Token</label>
|
||||
<div class="settings-input-wrap">
|
||||
<input type="password" id="google_ads_refresh_token" name="google_ads_refresh_token" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'google_ads_refresh_token' ) ); ?>" />
|
||||
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'google_ads_refresh_token' )">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="google_merchant_refresh_token">Merchant API Refresh Token</label>
|
||||
<div class="settings-input-wrap">
|
||||
<input type="password" id="google_merchant_refresh_token" name="google_merchant_refresh_token" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'google_merchant_refresh_token' ) ); ?>" placeholder="Refresh token dla scope content" />
|
||||
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'google_merchant_refresh_token' )">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="google_ads_manager_account_id">Manager Account ID <span class="text-muted">(opcjonalnie, dla MCC)</span></label>
|
||||
<input type="text" id="google_ads_manager_account_id" name="google_ads_manager_account_id" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'google_ads_manager_account_id' ) ); ?>" placeholder="np. 123-456-7890" />
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success" onclick="$( '#google-ads-settings' ).submit();"><i class="fa-solid fa-check mr5"></i>Zapisz ustawienia Google Ads</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 25px;">
|
||||
<div class="col-12">
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-icon"><i class="fa-solid fa-robot"></i></div>
|
||||
<div>
|
||||
<h3>OpenAI (ChatGPT)</h3>
|
||||
<small>Klucz API i model do optymalizacji tytułów i opisów produktów przez AI</small>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" id="openai-settings" action="/settings/save_openai">
|
||||
<div class="settings-field" style="margin-bottom: 16px;">
|
||||
<label class="settings-toggle-label">
|
||||
<?php $openai_enabled = \services\GoogleAdsApi::get_setting( 'openai_enabled' ) !== '0'; ?>
|
||||
<input type="hidden" name="openai_enabled" value="0" />
|
||||
<input type="checkbox" name="openai_enabled" value="1" class="settings-toggle-checkbox" <?= $openai_enabled ? 'checked' : ''; ?> />
|
||||
<span class="settings-toggle-switch"></span>
|
||||
<span class="settings-toggle-text">Włącz OpenAI (ChatGPT)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-fields-grid">
|
||||
<div class="settings-field">
|
||||
<label for="openai_api_key">API Key</label>
|
||||
<div class="settings-input-wrap">
|
||||
<input type="password" id="openai_api_key" name="openai_api_key" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'openai_api_key' ) ); ?>" placeholder="sk-..." />
|
||||
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'openai_api_key' )">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="openai_model">Model</label>
|
||||
<?php $current_model = \services\GoogleAdsApi::get_setting( 'openai_model' ) ?: 'gpt-5-mini'; ?>
|
||||
<select id="openai_model" name="openai_model" class="form-control">
|
||||
<option value="gpt-5.2" <?= $current_model === 'gpt-5.2' ? 'selected' : ''; ?>>GPT-5.2 (najnowszy, $1.75/$14 per 1M)</option>
|
||||
<option value="gpt-5-mini" <?= $current_model === 'gpt-5-mini' ? 'selected' : ''; ?>>GPT-5 Mini (szybki, $0.25/$2 per 1M)</option>
|
||||
<option value="gpt-4.1" <?= $current_model === 'gpt-4.1' ? 'selected' : ''; ?>>GPT-4.1</option>
|
||||
<option value="gpt-4.1-mini" <?= $current_model === 'gpt-4.1-mini' ? 'selected' : ''; ?>>GPT-4.1 Mini</option>
|
||||
<option value="gpt-4o" <?= $current_model === 'gpt-4o' ? 'selected' : ''; ?>>GPT-4o (legacy)</option>
|
||||
<option value="gpt-4o-mini" <?= $current_model === 'gpt-4o-mini' ? 'selected' : ''; ?>>GPT-4o Mini (legacy)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success" onclick="$( '#openai-settings' ).submit();"><i class="fa-solid fa-check mr5"></i>Zapisz ustawienia OpenAI</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 25px;">
|
||||
<div class="col-12">
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-icon"><i class="fa-solid fa-brain"></i></div>
|
||||
<div>
|
||||
<h3>Claude (Anthropic)</h3>
|
||||
<small>Klucz API i model Claude do optymalizacji tytułów i opisów produktów przez AI</small>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" id="claude-settings" action="/settings/save_claude">
|
||||
<div class="settings-field" style="margin-bottom: 16px;">
|
||||
<label class="settings-toggle-label">
|
||||
<?php $claude_enabled = \services\GoogleAdsApi::get_setting( 'claude_enabled' ) !== '0'; ?>
|
||||
<input type="hidden" name="claude_enabled" value="0" />
|
||||
<input type="checkbox" name="claude_enabled" value="1" class="settings-toggle-checkbox" <?= $claude_enabled ? 'checked' : ''; ?> />
|
||||
<span class="settings-toggle-switch"></span>
|
||||
<span class="settings-toggle-text">Włącz Claude (Anthropic)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="settings-fields-grid">
|
||||
<div class="settings-field">
|
||||
<label for="claude_api_key">API Key</label>
|
||||
<div class="settings-input-wrap">
|
||||
<input type="password" id="claude_api_key" name="claude_api_key" class="form-control" value="<?= htmlspecialchars( \services\GoogleAdsApi::get_setting( 'claude_api_key' ) ); ?>" placeholder="sk-ant-..." />
|
||||
<button type="button" class="settings-toggle-pw" onclick="password_toggle( this, 'claude_api_key' )">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="claude_model">Model</label>
|
||||
<?php $current_claude_model = \services\GoogleAdsApi::get_setting( 'claude_model' ) ?: 'claude-sonnet-4-5-20250929'; ?>
|
||||
<select id="claude_model" name="claude_model" class="form-control">
|
||||
<option value="claude-opus-4-6" <?= $current_claude_model === 'claude-opus-4-6' ? 'selected' : ''; ?>>Claude Opus 4.6 (najpotężniejszy)</option>
|
||||
<option value="claude-sonnet-4-5-20250929" <?= $current_claude_model === 'claude-sonnet-4-5-20250929' ? 'selected' : ''; ?>>Claude Sonnet 4.5 (zbalansowany)</option>
|
||||
<option value="claude-haiku-4-5-20251001" <?= $current_claude_model === 'claude-haiku-4-5-20251001' ? 'selected' : ''; ?>>Claude Haiku 4.5 (szybki, tani)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-success" onclick="$( '#claude-settings' ).submit();"><i class="fa-solid fa-check mr5"></i>Zapisz ustawienia Claude</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<script type="text/javascript">
|
||||
function password_toggle( btn, id )
|
||||
{
|
||||
var icon = btn.querySelector( 'i' );
|
||||
var input = document.getElementById( id );
|
||||
if ( !input || !icon )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ( input.type === 'password' )
|
||||
{
|
||||
@@ -206,4 +301,179 @@
|
||||
icon.classList.add( 'fa-eye' );
|
||||
}
|
||||
}
|
||||
|
||||
<?php if ( $settings_tab === 'cron' ): ?>
|
||||
(function()
|
||||
{
|
||||
var refresh_interval_ms = 60000;
|
||||
var refresh_timer = null;
|
||||
var cron_status_url = '/settings/cron_status';
|
||||
|
||||
function set_text( selector, value )
|
||||
{
|
||||
var node = document.querySelector( selector );
|
||||
if ( !node )
|
||||
{
|
||||
return;
|
||||
}
|
||||
node.textContent = value;
|
||||
}
|
||||
|
||||
function render_progress( progress )
|
||||
{
|
||||
if ( !Array.isArray( progress ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
progress.forEach( function( item, index )
|
||||
{
|
||||
var container = document.querySelector( '[data-cron-progress-item="' + index + '"]' );
|
||||
if ( !container )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var processed = parseInt( item && item.processed ? item.processed : 0, 10 );
|
||||
var total = parseInt( item && item.total ? item.total : 0, 10 );
|
||||
var percent = parseInt( item && item.percent ? item.percent : 0, 10 );
|
||||
if ( isNaN( processed ) ) processed = 0;
|
||||
if ( isNaN( total ) ) total = 0;
|
||||
if ( isNaN( percent ) ) percent = 0;
|
||||
percent = Math.max( 0, Math.min( 100, percent ) );
|
||||
|
||||
var summary = container.querySelector( '[data-cron-progress-summary]' );
|
||||
if ( summary )
|
||||
{
|
||||
summary.textContent = processed + ' / ' + total + ' (' + percent + '%)';
|
||||
}
|
||||
|
||||
var fill = container.querySelector( '[data-cron-progress-fill]' );
|
||||
if ( fill )
|
||||
{
|
||||
fill.style.width = percent + '%';
|
||||
}
|
||||
|
||||
var meta = container.querySelector( '[data-cron-progress-meta]' );
|
||||
if ( meta )
|
||||
{
|
||||
meta.textContent = item && item.meta ? item.meta : '';
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
function render_urls( urls )
|
||||
{
|
||||
if ( !Array.isArray( urls ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
urls.forEach( function( row, index )
|
||||
{
|
||||
var container = document.querySelector( '[data-cron-url-item="' + index + '"]' );
|
||||
if ( !container )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var last_invoked = container.querySelector( '[data-cron-url-last-invoked]' );
|
||||
if ( last_invoked )
|
||||
{
|
||||
last_invoked.textContent = row && row.last_invoked_at ? row.last_invoked_at : 'Brak danych';
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
function render_cron_data( data )
|
||||
{
|
||||
if ( !data || typeof data !== 'object' )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
set_text( '[data-cron-overall-last-invoked]', data.overall_last_invoked_at || 'Brak danych' );
|
||||
set_text( '[data-cron-clients-total]', data.clients_total || 0 );
|
||||
render_progress( data.progress );
|
||||
render_urls( data.urls );
|
||||
}
|
||||
|
||||
function schedule_refresh()
|
||||
{
|
||||
if ( refresh_timer )
|
||||
{
|
||||
clearTimeout( refresh_timer );
|
||||
}
|
||||
refresh_timer = setTimeout( refresh_status, refresh_interval_ms );
|
||||
}
|
||||
|
||||
function refresh_status()
|
||||
{
|
||||
if ( document.hidden )
|
||||
{
|
||||
schedule_refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( typeof window.fetch !== 'function' )
|
||||
{
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
fetch( cron_status_url + '?_ts=' + Date.now(), {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
} )
|
||||
.then( function( response )
|
||||
{
|
||||
if ( !response.ok )
|
||||
{
|
||||
throw new Error( 'HTTP ' + response.status );
|
||||
}
|
||||
return response.json();
|
||||
} )
|
||||
.then( function( payload )
|
||||
{
|
||||
if ( payload && payload.status === 'ok' )
|
||||
{
|
||||
render_cron_data( payload.data || {} );
|
||||
}
|
||||
} )
|
||||
.catch( function()
|
||||
{
|
||||
} )
|
||||
.finally( function()
|
||||
{
|
||||
schedule_refresh();
|
||||
} );
|
||||
}
|
||||
|
||||
document.addEventListener( 'visibilitychange', function()
|
||||
{
|
||||
if ( document.hidden )
|
||||
{
|
||||
if ( refresh_timer )
|
||||
{
|
||||
clearTimeout( refresh_timer );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
refresh_status();
|
||||
} );
|
||||
|
||||
window.addEventListener( 'beforeunload', function()
|
||||
{
|
||||
if ( refresh_timer )
|
||||
{
|
||||
clearTimeout( refresh_timer );
|
||||
}
|
||||
} );
|
||||
|
||||
schedule_refresh();
|
||||
})();
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
|
||||
67
templates/xml_files/main_view.php
Normal file
67
templates/xml_files/main_view.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<div class="clients-page xml-files-page">
|
||||
<div class="clients-header">
|
||||
<h2><i class="fa-solid fa-file-code"></i> Pliki XML</h2>
|
||||
</div>
|
||||
|
||||
<div class="clients-table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 60px;">#ID</th>
|
||||
<th>Klient</th>
|
||||
<th>Google Ads ID</th>
|
||||
<th>Link do custom feed</th>
|
||||
<th style="width: 180px;">Ostatnia modyfikacja</th>
|
||||
<th style="width: 220px; text-align: center;">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ( $this -> rows ): ?>
|
||||
<?php foreach ( $this -> rows as $row ): ?>
|
||||
<tr>
|
||||
<td class="client-id"><?= (int) ( $row['client_id'] ?? 0 ); ?></td>
|
||||
<td class="client-name"><?= htmlspecialchars( (string) ( $row['client_name'] ?? '' ) ); ?></td>
|
||||
<td>
|
||||
<?php if ( !empty( $row['google_ads_customer_id'] ) ): ?>
|
||||
<span class="badge-id"><?= htmlspecialchars( (string) $row['google_ads_customer_id'] ); ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">- brak -</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<?= htmlspecialchars( (string) ( $row['xml_url'] ?? '' ) ); ?>" target="_blank" rel="noopener">
|
||||
<?= htmlspecialchars( (string) ( $row['xml_url'] ?? '' ) ); ?>
|
||||
</a>
|
||||
<?php if ( empty( $row['xml_exists'] ) ): ?>
|
||||
<div class="text-muted">Plik nie zostal jeszcze wygenerowany.</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ( !empty( $row['xml_last_modified'] ) ): ?>
|
||||
<?= htmlspecialchars( (string) $row['xml_last_modified'] ); ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">Brak</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="actions-cell" style="justify-content: center;">
|
||||
<form method="POST" action="/xml_files/regenerate" style="margin: 0;">
|
||||
<input type="hidden" name="client_id" value="<?= (int) ( $row['client_id'] ?? 0 ); ?>">
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="fa-solid fa-rotate-right mr5"></i> Wymus generowanie
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="6" class="empty-state">
|
||||
<i class="fa-solid fa-file-code"></i>
|
||||
<p>Brak klientow do wyswietlenia.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
30
tmp/debug_products_urls.php
Normal file
30
tmp/debug_products_urls.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
require __DIR__ . '/../config.php';
|
||||
require __DIR__ . '/../libraries/medoo/medoo.php';
|
||||
|
||||
$mdb = new medoo([
|
||||
'database_type' => 'mysql',
|
||||
'database_name' => $database['name'],
|
||||
'server' => $database['host'],
|
||||
'username' => $database['user'],
|
||||
'password' => $database['password'],
|
||||
'charset' => 'utf8'
|
||||
]);
|
||||
|
||||
$client_id = 5;
|
||||
|
||||
$rows = [];
|
||||
$rows['products_total'] = (int)$mdb->query("SELECT COUNT(*) FROM products WHERE client_id = {$client_id}")->fetchColumn();
|
||||
$rows['products_not_deleted'] = (int)$mdb->query("SELECT COUNT(*) FROM products WHERE client_id = {$client_id} AND (deleted = 0 OR deleted IS NULL)")->fetchColumn();
|
||||
$rows['products_with_offer'] = (int)$mdb->query("SELECT COUNT(*) FROM products WHERE client_id = {$client_id} AND (deleted = 0 OR deleted IS NULL) AND TRIM(COALESCE(offer_id,'')) <> ''")->fetchColumn();
|
||||
$rows['pd_rows_for_client_products'] = (int)$mdb->query("SELECT COUNT(*) FROM products_data pd JOIN products p ON p.id = pd.product_id WHERE p.client_id = {$client_id}")->fetchColumn();
|
||||
$rows['pd_distinct_products'] = (int)$mdb->query("SELECT COUNT(DISTINCT pd.product_id) FROM products_data pd JOIN products p ON p.id = pd.product_id WHERE p.client_id = {$client_id}")->fetchColumn();
|
||||
$rows['products_with_real_url_any_pd_row'] = (int)$mdb->query("SELECT COUNT(DISTINCT p.id) FROM products p JOIN products_data pd ON pd.product_id = p.id WHERE p.client_id = {$client_id} AND (p.deleted = 0 OR p.deleted IS NULL) AND TRIM(COALESCE(p.offer_id,'')) <> '' AND TRIM(COALESCE(pd.product_url,'')) <> '' AND LOWER(TRIM(pd.product_url)) NOT IN ('0','-','null')")->fetchColumn();
|
||||
$rows['products_without_real_url'] = (int)$mdb->query("SELECT COUNT(*) FROM products p LEFT JOIN ( SELECT product_id, MAX(CASE WHEN TRIM(COALESCE(product_url,'')) = '' THEN 0 WHEN LOWER(TRIM(product_url)) IN ('0','-','null') THEN 0 ELSE 1 END) AS has_real_url FROM products_data GROUP BY product_id ) pd ON pd.product_id = p.id WHERE p.client_id = {$client_id} AND (p.deleted = 0 OR p.deleted IS NULL) AND TRIM(COALESCE(p.offer_id,'')) <> '' AND COALESCE(pd.has_real_url,0) = 0")->fetchColumn();
|
||||
|
||||
$dup = $mdb->query("SELECT COUNT(*) FROM (SELECT pd.product_id, COUNT(*) c FROM products_data pd JOIN products p ON p.id = pd.product_id WHERE p.client_id = {$client_id} GROUP BY pd.product_id HAVING COUNT(*) > 1) t")->fetchColumn();
|
||||
$rows['products_with_duplicate_pd_rows'] = (int)$dup;
|
||||
|
||||
$sample = $mdb->query("SELECT p.id, p.offer_id, pd.product_url FROM products p LEFT JOIN products_data pd ON pd.product_id=p.id WHERE p.client_id={$client_id} ORDER BY p.id DESC LIMIT 10")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['stats'=>$rows,'sample'=>$sample], JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);
|
||||
Reference in New Issue
Block a user