diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..099e738
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,14 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(npx sass:*)",
+ "Bash(npx md-to-pdf:*)",
+ "WebFetch(domain:support.google.com)",
+ "WebFetch(domain:feedops.com)",
+ "WebFetch(domain:help.kliken.com)",
+ "WebFetch(domain:www.storegrowers.com)",
+ "WebFetch(domain:platform.openai.com)",
+ "WebFetch(domain:openai.com)"
+ ]
+ }
+}
diff --git a/.htaccess b/.htaccess
index 2178078..f273f58 100644
--- a/.htaccess
+++ b/.htaccess
@@ -8,10 +8,14 @@ RewriteRule ^(.*)$ https://%1/$1 [R=301,L]
RewriteCond %{SERVER_PORT} !=443
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=permanent]
-RewriteCond %{REQUEST_URI} !^(.*)/libraries/(.*) [NC]
-RewriteCond %{REQUEST_URI} !^(.*)/temp/(.*) [NC]
-RewriteCond %{REQUEST_URI} !^(.*)/layout/(.*) [NC]
-RewriteCond %{REQUEST_URI} !^(.*)/upload/(.*) [NC]
-RewriteRule ^([^/]*)/([^/]*)/(.*)$ index.php?module=$1&action=$2&$3 [L]
+# Statyczne zasoby - pomijaj
+RewriteCond %{REQUEST_URI} ^/(libraries|layout|upload|temp)/ [NC]
+RewriteRule ^ - [L]
-RewriteRule ^logowanie$ index.php?module=users&action=login_form [L]
\ No newline at end of file
+# Istniejące pliki/katalogi - pomijaj
+RewriteCond %{REQUEST_FILENAME} -f [OR]
+RewriteCond %{REQUEST_FILENAME} -d
+RewriteRule ^ - [L]
+
+# Wszystko inne → index.php
+RewriteRule ^(.*)$ index.php [L,QSA]
diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json
index 6e32d03..1eb8b0a 100644
--- a/.vscode/ftp-kr.sync.cache.json
+++ b/.vscode/ftp-kr.sync.cache.json
@@ -161,8 +161,8 @@
"products": {
"main_view.php": {
"type": "-",
- "size": 19004,
- "lmtime": 1769727759481,
+ "size": 19064,
+ "lmtime": 1770756800564,
"modified": false
},
"product_history.php": {
diff --git a/autoload/controls/class.Clients.php b/autoload/controls/class.Clients.php
new file mode 100644
index 0000000..9f041fb
--- /dev/null
+++ b/autoload/controls/class.Clients.php
@@ -0,0 +1,70 @@
+ $name,
+ 'google_ads_customer_id' => $google_ads_customer_id ?: null,
+ 'google_ads_start_date' => $google_ads_start_date ?: null,
+ ];
+
+ if ( $id )
+ {
+ \factory\Clients::update( $id, $data );
+ \S::alert( 'Klient został zaktualizowany.' );
+ }
+ else
+ {
+ \factory\Clients::create( $data );
+ \S::alert( 'Klient został dodany.' );
+ }
+
+ header( 'Location: /clients' );
+ exit;
+ }
+
+ static public function delete()
+ {
+ $id = \S::get( 'id' );
+
+ if ( $id )
+ {
+ \factory\Clients::delete( $id );
+ }
+
+ echo json_encode( [ 'success' => true ] );
+ exit;
+ }
+
+ static public function get()
+ {
+ $id = \S::get( 'id' );
+ $client = \factory\Clients::get( $id );
+
+ echo json_encode( $client ?: [] );
+ exit;
+ }
+}
diff --git a/autoload/controls/class.Cron.php b/autoload/controls/class.Cron.php
index 1d7279c..06d60bc 100644
--- a/autoload/controls/class.Cron.php
+++ b/autoload/controls/class.Cron.php
@@ -516,6 +516,160 @@ class Cron
exit;
}
+ // ===========================
+ // KAMPANIE - Google Ads API
+ // ===========================
+
+ static public function cron_campaigns()
+ {
+ global $mdb;
+
+ $api = new \services\GoogleAdsApi();
+
+ if ( !$api -> is_configured() )
+ {
+ echo json_encode( [ 'result' => 'Google Ads API nie jest skonfigurowane. Uzupelnij dane w Ustawieniach.' ] );
+ exit;
+ }
+
+ // Pobierz klientów z ustawionym Google Ads Customer ID
+ $clients = $mdb -> select( 'clients', '*', [
+ 'google_ads_customer_id[!]' => null
+ ] );
+
+ if ( empty( $clients ) )
+ {
+ echo json_encode( [ 'result' => 'Brak klientow z ustawionym Google Ads Customer ID.' ] );
+ exit;
+ }
+
+ $today = date( 'Y-m-d' );
+ $processed = 0;
+ $errors = [];
+
+ foreach ( $clients as $client )
+ {
+ $customer_id = $client['google_ads_customer_id'];
+
+ // Pobierz dane 30-dniowe
+ $campaigns_30 = $api -> get_campaigns_30_days( $customer_id );
+ if ( $campaigns_30 === false )
+ {
+ $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
+ $errors[] = 'Blad API dla klienta ' . $client['name'] . ' (ID: ' . $customer_id . '): ' . $last_err;
+ continue;
+ }
+
+ // Pobierz dane all-time
+ $campaigns_all_time = $api -> get_campaigns_all_time( $customer_id );
+ $all_time_map = [];
+ if ( is_array( $campaigns_all_time ) )
+ {
+ foreach ( $campaigns_all_time as $cat )
+ {
+ $all_time_map[ $cat['campaign_id'] ] = $cat['roas_all_time'];
+ }
+ }
+
+ foreach ( $campaigns_30 as $campaign )
+ {
+ // Upsert kampanii
+ if ( !$mdb -> count( 'campaigns', [ 'AND' => [
+ 'client_id' => $client['id'],
+ 'campaign_id' => $campaign['campaign_id']
+ ] ] ) )
+ {
+ $mdb -> insert( 'campaigns', [
+ 'client_id' => $client['id'],
+ 'campaign_id' => $campaign['campaign_id'],
+ 'campaign_name' => $campaign['campaign_name']
+ ] );
+ $db_campaign_id = $mdb -> id();
+ }
+ else
+ {
+ $db_campaign_id = $mdb -> get( 'campaigns', 'id', [ 'AND' => [
+ 'client_id' => $client['id'],
+ 'campaign_id' => $campaign['campaign_id']
+ ] ] );
+
+ // Aktualizuj nazwe kampanii jesli sie zmienila
+ $mdb -> update( 'campaigns', [
+ 'campaign_name' => $campaign['campaign_name']
+ ], [ 'id' => $db_campaign_id ] );
+ }
+
+ // Budowanie strategii biddingu
+ $bidding_strategy = self::format_bidding_strategy(
+ $campaign['bidding_strategy'],
+ $campaign['target_roas'] ?? 0
+ );
+
+ // Dane historii
+ $history_data = [
+ 'roas_30_days' => $campaign['roas_30_days'],
+ 'roas_all_time' => $all_time_map[ $campaign['campaign_id'] ] ?? 0,
+ 'budget' => $campaign['budget'],
+ 'money_spent' => $campaign['money_spent'],
+ 'conversion_value' => $campaign['conversion_value'],
+ 'bidding_strategy' => $bidding_strategy,
+ ];
+
+ // Upsert do campaigns_history
+ if ( $mdb -> count( 'campaigns_history', [ 'AND' => [
+ 'campaign_id' => $db_campaign_id,
+ 'date_add' => $today
+ ] ] ) )
+ {
+ $mdb -> update( 'campaigns_history', $history_data, [ 'AND' => [
+ 'campaign_id' => $db_campaign_id,
+ 'date_add' => $today
+ ] ] );
+ }
+ else
+ {
+ $history_data['campaign_id'] = $db_campaign_id;
+ $history_data['date_add'] = $today;
+ $mdb -> insert( 'campaigns_history', $history_data );
+ }
+
+ $processed++;
+ }
+ }
+
+ echo json_encode( [
+ 'result' => 'Synchronizacja zakonczona. Przetworzono kampanii: ' . $processed . '.',
+ 'errors' => $errors
+ ] );
+ exit;
+ }
+
+ static private function format_bidding_strategy( $strategy_type, $target_roas = 0 )
+ {
+ $map = [
+ 'MAXIMIZE_CONVERSIONS' => 'Maksymalizacja liczby konwersji',
+ 'MAXIMIZE_CONVERSION_VALUE' => 'Maksymalizacja wartosci konwersji',
+ 'TARGET_ROAS' => 'Docelowy ROAS',
+ 'TARGET_CPA' => 'Docelowy CPA',
+ 'MANUAL_CPC' => 'Reczny CPC',
+ 'MANUAL_CPM' => 'Reczny CPM',
+ 'TARGET_IMPRESSION_SHARE' => 'Docelowy udzial w wyswietleniach',
+ ];
+
+ $label = $map[ $strategy_type ] ?? $strategy_type ?? 'brak';
+
+ if ( $target_roas > 0 )
+ {
+ $label .= ' | docelowy ROAS: ' . round( $target_roas * 100 ) . '%';
+ }
+
+ return $label;
+ }
+
+ // ===========================
+ // FRAZY - history 30
+ // ===========================
+
static public function cron_phrase_history_30_save( $phrase_id, $date_from, $date_to )
{
global $mdb;
diff --git a/autoload/controls/class.Products.php b/autoload/controls/class.Products.php
index 4f76e12..cc77640 100644
--- a/autoload/controls/class.Products.php
+++ b/autoload/controls/class.Products.php
@@ -69,8 +69,82 @@ class Products
$product_title = \factory\Products::get_product_data( $product_id, 'title' );
$product_description = \factory\Products::get_product_data( $product_id, 'description' );
+ $google_product_category = \factory\Products::get_product_data( $product_id, 'google_product_category' );
+ $product_url = \factory\Products::get_product_data( $product_id, 'product_url' );
- echo json_encode( [ 'status' => 'ok', 'product_details' => [ 'title' => $product_title, 'description' => $product_description ] ] );
+ echo json_encode( [ 'status' => 'ok', 'product_details' => [
+ 'title' => $product_title,
+ 'description' => $product_description,
+ 'google_product_category' => $google_product_category,
+ 'product_url' => $product_url
+ ] ] );
+ exit;
+ }
+
+ static public function ai_suggest()
+ {
+ $product_id = \S::get( 'product_id' );
+ $field = \S::get( 'field' );
+
+ if ( !\services\OpenAiApi::is_configured() )
+ {
+ echo json_encode( [ 'status' => 'error', 'message' => 'Klucz API OpenAI nie jest skonfigurowany. Przejdź do Ustawień.' ] );
+ exit;
+ }
+
+ $product = \factory\Products::get_product_full_context( $product_id );
+ if ( !$product )
+ {
+ echo json_encode( [ 'status' => 'error', 'message' => 'Nie znaleziono produktu.' ] );
+ exit;
+ }
+
+ // Pobierz treść strony produktu jeśli podano URL
+ $product_url = \S::get( 'product_url' );
+ $page_content = '';
+ if ( $product_url && filter_var( $product_url, FILTER_VALIDATE_URL ) )
+ {
+ $page_content = \services\OpenAiApi::fetch_page_content( $product_url );
+ }
+
+ $context = [
+ 'original_name' => $product['name'],
+ 'current_title' => \factory\Products::get_product_data( $product_id, 'title' ),
+ 'current_description' => \factory\Products::get_product_data( $product_id, 'description' ),
+ 'current_category' => \factory\Products::get_product_data( $product_id, 'google_product_category' ),
+ 'offer_id' => $product['offer_id'],
+ 'impressions_30' => $product['impressions_30'] ?? 0,
+ 'clicks_30' => $product['clicks_30'] ?? 0,
+ 'ctr' => $product['ctr'] ?? 0,
+ 'cost' => $product['cost'] ?? 0,
+ 'conversions' => $product['conversions'] ?? 0,
+ 'conversions_value' => $product['conversions_value'] ?? 0,
+ 'roas' => $product['roas'] ?? 0,
+ 'custom_label_4' => \factory\Products::get_product_data( $product_id, 'custom_label_4' ),
+ 'page_content' => $page_content,
+ ];
+
+ switch ( $field )
+ {
+ case 'title':
+ $result = \services\OpenAiApi::suggest_title( $context );
+ break;
+ case 'description':
+ $result = \services\OpenAiApi::suggest_description( $context );
+ break;
+ case 'category':
+ $result = \services\OpenAiApi::suggest_category( $context );
+ break;
+ default:
+ $result = [ 'status' => 'error', 'message' => 'Nieznane pole: ' . $field ];
+ }
+
+ if ( $product_url && !$page_content )
+ $result['warning'] = 'Nie udało się pobrać treści ze strony produktu (strona może blokować dostęp). Sugestia oparta tylko na nazwie produktu.';
+ elseif ( $page_content )
+ $result['page_fetched'] = true;
+
+ echo json_encode( $result );
exit;
}
@@ -191,7 +265,7 @@ class Products
'',
'',
'',
- 'Usuń'
+ ''
];
}
@@ -386,15 +460,21 @@ class Products
$custom_title = \S::get( 'custom_title' );
$custom_description = \S::get( 'custom_description' );
$google_product_category = \S::get( 'google_product_category' );
+ $product_url = \S::get( 'product_url' );
- if ( $product_id and $custom_title )
- \factory\Products::set_product_data( $product_id, 'title', $custom_title );
+ if ( $product_id )
+ {
+ if ( $custom_title )
+ \factory\Products::set_product_data( $product_id, 'title', $custom_title );
- if ( $product_id and $custom_description )
- \factory\Products::set_product_data( $product_id, 'description', $custom_description );
+ if ( $custom_description )
+ \factory\Products::set_product_data( $product_id, 'description', $custom_description );
- if ( $product_id and $google_product_category )
- \factory\Products::set_product_data( $product_id, 'google_product_category', $google_product_category );
+ if ( $google_product_category )
+ \factory\Products::set_product_data( $product_id, 'google_product_category', $google_product_category );
+
+ \factory\Products::set_product_data( $product_id, 'product_url', $product_url ?: '' );
+ }
\factory\Products::add_product_comment( $product_id, 'Zmiana tytułu i opisu produktu.' );
diff --git a/autoload/controls/class.Users.php b/autoload/controls/class.Users.php
index 3e6d01d..dd2ee36 100644
--- a/autoload/controls/class.Users.php
+++ b/autoload/controls/class.Users.php
@@ -56,7 +56,7 @@ class Users
\S::set_session( 'user', $user );
\S::alert( 'Ustawienia zostały zapisane.' );
}
- header( 'Location: /users/settings/' );
+ header( 'Location: /settings' );
exit;
}
@@ -66,7 +66,8 @@ class Users
if ( !$user )
{
- return \Tpl::view( 'users/login-form' );
+ header( 'Location: /login' );
+ exit;
}
return \view\Users::settings(
@@ -74,6 +75,40 @@ class Users
);
}
+ public static function settings_save_google_ads()
+ {
+ $fields = [
+ 'google_ads_developer_token',
+ 'google_ads_client_id',
+ 'google_ads_client_secret',
+ 'google_ads_refresh_token',
+ 'google_ads_manager_account_id',
+ ];
+
+ foreach ( $fields as $field )
+ {
+ \services\GoogleAdsApi::set_setting( $field, \S::get( $field ) );
+ }
+
+ // 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 );
+
+ \S::alert( 'Ustawienia Google Ads zostały zapisane.' );
+ header( 'Location: /settings' );
+ exit;
+ }
+
+ public static function settings_save_openai()
+ {
+ \services\GoogleAdsApi::set_setting( 'openai_api_key', \S::get( 'openai_api_key' ) );
+ \services\GoogleAdsApi::set_setting( 'openai_model', \S::get( 'openai_model' ) );
+
+ \S::alert( 'Ustawienia OpenAI zostały zapisane.' );
+ header( 'Location: /settings' );
+ exit;
+ }
+
public static function login()
{
if ( $user = \factory\Users::login(
diff --git a/autoload/factory/class.Clients.php b/autoload/factory/class.Clients.php
new file mode 100644
index 0000000..6c188f5
--- /dev/null
+++ b/autoload/factory/class.Clients.php
@@ -0,0 +1,36 @@
+ select( 'clients', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
+ }
+
+ static public function get( $id )
+ {
+ global $mdb;
+ return $mdb -> get( 'clients', '*', [ 'id' => $id ] );
+ }
+
+ static public function create( $data )
+ {
+ global $mdb;
+ $mdb -> insert( 'clients', $data );
+ return $mdb -> id();
+ }
+
+ static public function update( $id, $data )
+ {
+ global $mdb;
+ return $mdb -> update( 'clients', $data, [ 'id' => $id ] );
+ }
+
+ static public function delete( $id )
+ {
+ global $mdb;
+ return $mdb -> delete( 'clients', [ 'id' => $id ] );
+ }
+}
diff --git a/autoload/factory/class.Products.php b/autoload/factory/class.Products.php
index 567be87..2c2cbb2 100644
--- a/autoload/factory/class.Products.php
+++ b/autoload/factory/class.Products.php
@@ -108,6 +108,20 @@ class Products
return $mdb -> query( 'SELECT COUNT(0) FROM products_temp AS pt INNER JOIN products AS p ON p.id = pt.product_id WHERE client_id = \'' . $client_id . '\'' ) -> fetchColumn();
}
+ static public function get_product_full_context( $product_id )
+ {
+ global $mdb;
+ return $mdb -> query(
+ 'SELECT p.id, p.offer_id, p.name, p.min_roas,
+ pt.impressions, pt.impressions_30, pt.clicks, pt.clicks_30,
+ pt.ctr, pt.cost, pt.cpc, pt.conversions, pt.conversions_value, pt.roas
+ FROM products AS p
+ LEFT JOIN products_temp AS pt ON pt.product_id = p.id
+ WHERE p.id = :pid',
+ [ ':pid' => $product_id ]
+ ) -> fetch( \PDO::FETCH_ASSOC );
+ }
+
static public function get_product_data( $product_id, $field )
{
global $mdb;
diff --git a/autoload/services/class.GoogleAdsApi.php b/autoload/services/class.GoogleAdsApi.php
new file mode 100644
index 0000000..22cdcd1
--- /dev/null
+++ b/autoload/services/class.GoogleAdsApi.php
@@ -0,0 +1,274 @@
+ developer_token = self::get_setting( 'google_ads_developer_token' );
+ $this -> client_id = self::get_setting( 'google_ads_client_id' );
+ $this -> client_secret = self::get_setting( 'google_ads_client_secret' );
+ $this -> refresh_token = self::get_setting( 'google_ads_refresh_token' );
+ $this -> manager_account_id = self::get_setting( 'google_ads_manager_account_id' );
+ }
+
+ // --- Settings CRUD ---
+
+ public static function get_setting( $key )
+ {
+ global $mdb;
+ return $mdb -> get( 'settings', 'setting_value', [ 'setting_key' => $key ] );
+ }
+
+ public static function set_setting( $key, $value )
+ {
+ global $mdb;
+
+ if ( $mdb -> count( 'settings', [ 'setting_key' => $key ] ) )
+ {
+ $mdb -> update( 'settings', [ 'setting_value' => $value ], [ 'setting_key' => $key ] );
+ }
+ else
+ {
+ $mdb -> insert( 'settings', [ 'setting_key' => $key, 'setting_value' => $value ] );
+ }
+ }
+
+ // --- Konfiguracja ---
+
+ public function is_configured()
+ {
+ return !empty( $this -> developer_token )
+ && !empty( $this -> client_id )
+ && !empty( $this -> client_secret )
+ && !empty( $this -> refresh_token );
+ }
+
+ // --- OAuth2 ---
+
+ private function get_access_token()
+ {
+ $cached_token = self::get_setting( 'google_ads_access_token' );
+ $cached_expires = (int) self::get_setting( 'google_ads_access_token_expires' );
+
+ if ( $cached_token && $cached_expires > time() )
+ {
+ $this -> access_token = $cached_token;
+ return $this -> access_token;
+ }
+
+ return $this -> refresh_access_token();
+ }
+
+ private function refresh_access_token()
+ {
+ $ch = curl_init( self::$TOKEN_URL );
+ curl_setopt_array( $ch, [
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => http_build_query( [
+ 'client_id' => $this -> client_id,
+ 'client_secret' => $this -> client_secret,
+ 'refresh_token' => $this -> refresh_token,
+ 'grant_type' => 'refresh_token'
+ ] ),
+ CURLOPT_SSL_VERIFYPEER => true,
+ CURLOPT_TIMEOUT => 30,
+ ] );
+
+ $response = curl_exec( $ch );
+ $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
+ $error = curl_error( $ch );
+ curl_close( $ch );
+
+ if ( $http_code !== 200 || !$response )
+ {
+ self::set_setting( 'google_ads_last_error', 'Token refresh failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . $response );
+ return false;
+ }
+
+ $data = json_decode( $response, true );
+
+ if ( !isset( $data['access_token'] ) )
+ {
+ self::set_setting( 'google_ads_last_error', 'Token refresh: brak access_token w odpowiedzi' );
+ return false;
+ }
+
+ $this -> access_token = $data['access_token'];
+ $expires_at = time() + ( $data['expires_in'] ?? 3600 ) - 60;
+
+ self::set_setting( 'google_ads_access_token', $this -> access_token );
+ self::set_setting( 'google_ads_access_token_expires', $expires_at );
+ self::set_setting( 'google_ads_last_error', null );
+
+ return $this -> access_token;
+ }
+
+ // --- Google Ads API ---
+
+ public function search_stream( $customer_id, $gaql_query )
+ {
+ $access_token = $this -> get_access_token();
+ if ( !$access_token ) return false;
+
+ $customer_id = str_replace( '-', '', $customer_id );
+
+ $url = self::$ADS_BASE_URL . '/' . self::$API_VERSION
+ . '/customers/' . $customer_id . '/googleAds:searchStream';
+
+ $headers = [
+ 'Authorization: Bearer ' . $access_token,
+ 'developer-token: ' . $this -> developer_token,
+ 'Content-Type: application/json',
+ ];
+
+ if ( !empty( $this -> manager_account_id ) )
+ {
+ $headers[] = 'login-customer-id: ' . str_replace( '-', '', $this -> manager_account_id );
+ }
+
+ $ch = curl_init( $url );
+ curl_setopt_array( $ch, [
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_POST => true,
+ CURLOPT_HTTPHEADER => $headers,
+ CURLOPT_POSTFIELDS => json_encode( [ 'query' => $gaql_query ] ),
+ CURLOPT_SSL_VERIFYPEER => true,
+ CURLOPT_TIMEOUT => 120,
+ ] );
+
+ $response = curl_exec( $ch );
+ $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
+ $error = curl_error( $ch );
+ curl_close( $ch );
+
+ if ( $http_code !== 200 || !$response )
+ {
+ self::set_setting( 'google_ads_last_error', 'searchStream failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . substr( $response, 0, 500 ) );
+ return false;
+ }
+
+ $data = json_decode( $response, true );
+
+ // searchStream zwraca tablicę batch'y, każdy z kluczem 'results'
+ $results = [];
+ if ( is_array( $data ) )
+ {
+ foreach ( $data as $batch )
+ {
+ if ( isset( $batch['results'] ) && is_array( $batch['results'] ) )
+ {
+ $results = array_merge( $results, $batch['results'] );
+ }
+ }
+ }
+
+ self::set_setting( 'google_ads_last_error', null );
+
+ return $results;
+ }
+
+ // --- Kampanie: dane 30-dniowe ---
+
+ public function get_campaigns_30_days( $customer_id )
+ {
+ $gaql = "SELECT "
+ . "campaign.id, "
+ . "campaign.name, "
+ . "campaign.bidding_strategy_type, "
+ . "campaign.target_roas.target_roas, "
+ . "campaign_budget.amount_micros, "
+ . "metrics.cost_micros, "
+ . "metrics.conversions_value "
+ . "FROM campaign "
+ . "WHERE campaign.status = 'ENABLED' "
+ . "AND segments.date DURING LAST_30_DAYS";
+
+ $results = $this -> search_stream( $customer_id, $gaql );
+ if ( $results === false ) return false;
+
+ // Agregacja po campaign.id (API zwraca wiersz per dzień per kampania)
+ $campaigns = [];
+ foreach ( $results as $row )
+ {
+ $cid = $row['campaign']['id'] ?? null;
+ if ( !$cid ) continue;
+
+ if ( !isset( $campaigns[ $cid ] ) )
+ {
+ $campaigns[ $cid ] = [
+ 'campaign_id' => $cid,
+ 'campaign_name' => $row['campaign']['name'] ?? '',
+ 'bidding_strategy' => $row['campaign']['biddingStrategyType'] ?? 'UNKNOWN',
+ 'target_roas' => isset( $row['campaign']['targetRoas']['targetRoas'] )
+ ? (float) $row['campaign']['targetRoas']['targetRoas']
+ : 0,
+ 'budget' => isset( $row['campaignBudget']['amountMicros'] )
+ ? (float) $row['campaignBudget']['amountMicros'] / 1000000
+ : 0,
+ 'cost_total' => 0,
+ 'conversion_value' => 0,
+ ];
+ }
+
+ $campaigns[ $cid ]['cost_total'] += (float) ( $row['metrics']['costMicros'] ?? 0 );
+ $campaigns[ $cid ]['conversion_value'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
+ }
+
+ // Przeliczenie micros i ROAS
+ foreach ( $campaigns as &$c )
+ {
+ $c['money_spent'] = $c['cost_total'] / 1000000;
+ $c['roas_30_days'] = ( $c['money_spent'] > 0 )
+ ? round( ( $c['conversion_value'] / $c['money_spent'] ) * 100, 2 )
+ : 0;
+ unset( $c['cost_total'] );
+ }
+
+ return array_values( $campaigns );
+ }
+
+ // --- Kampanie: dane all-time ---
+
+ public function get_campaigns_all_time( $customer_id )
+ {
+ $gaql = "SELECT "
+ . "campaign.id, "
+ . "metrics.cost_micros, "
+ . "metrics.conversions_value "
+ . "FROM campaign "
+ . "WHERE campaign.status = 'ENABLED'";
+
+ $results = $this -> search_stream( $customer_id, $gaql );
+ if ( $results === false ) return false;
+
+ $campaigns = [];
+ foreach ( $results as $row )
+ {
+ $cid = $row['campaign']['id'] ?? null;
+ if ( !$cid ) continue;
+
+ $cost = (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
+ $value = (float) ( $row['metrics']['conversionsValue'] ?? 0 );
+
+ $campaigns[] = [
+ 'campaign_id' => $cid,
+ 'roas_all_time' => ( $cost > 0 ) ? round( ( $value / $cost ) * 100, 2 ) : 0,
+ ];
+ }
+
+ return $campaigns;
+ }
+}
diff --git a/autoload/services/class.OpenAiApi.php b/autoload/services/class.OpenAiApi.php
new file mode 100644
index 0000000..636becd
--- /dev/null
+++ b/autoload/services/class.OpenAiApi.php
@@ -0,0 +1,282 @@
+ true,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_TIMEOUT => 10,
+ CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
+ CURLOPT_HTTPHEADER => [
+ 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'Accept-Language: pl-PL,pl;q=0.9,en;q=0.8',
+ ],
+ CURLOPT_SSL_VERIFYPEER => false
+ ] );
+
+ $html = curl_exec( $ch );
+ $http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
+ curl_close( $ch );
+
+ if ( $http_code !== 200 || !$html ) return '';
+
+ // Usuń skrypty, style, komentarze
+ $html = preg_replace( '/
\ No newline at end of file
+
+ // Usuwanie kampanii
+ $( 'body' ).on( 'click', '#delete_campaign', function()
+ {
+ var campaign_id = $( '#campaign_id' ).val();
+ var campaign_name = $( '#campaign_id option:selected' ).text();
+
+ if ( !campaign_id )
+ {
+ $.alert({
+ title: 'Uwaga',
+ content: 'Najpierw wybierz kampanię do usunięcia.',
+ type: 'orange'
+ });
+ return;
+ }
+
+ $.confirm({
+ title: 'Potwierdzenie usunięcia',
+ content: 'Czy na pewno chcesz usunąć kampanię ' + campaign_name + '?
Ta operacja jest nieodwracalna i usunie również całą historię kampanii.',
+ type: 'red',
+ buttons: {
+ confirm: {
+ text: 'Usuń',
+ btnClass: 'btn-red',
+ keys: ['enter'],
+ action: function()
+ {
+ $.ajax({
+ url: '/campaigns/delete_campaign/campaign_id=' + campaign_id,
+ type: 'POST',
+ success: function( response )
+ {
+ var data = JSON.parse( response );
+ if ( data.success )
+ {
+ $.alert({
+ title: 'Sukces',
+ content: 'Kampania została usunięta.',
+ type: 'green',
+ autoClose: 'ok|2000'
+ });
+ $( '#client_id' ).trigger( 'change' );
+ }
+ else
+ {
+ $.alert({
+ title: 'Błąd',
+ content: data.message || 'Nie udało się usunąć kampanii.',
+ type: 'red'
+ });
+ }
+ }
+ });
+ }
+ },
+ cancel: { text: 'Anuluj' }
+ }
+ });
+ });
+
+ // Usuwanie wpisu historii
+ $( 'body' ).on( 'click', '.delete-history-entry', function()
+ {
+ var btn = $( this );
+ var history_id = btn.data( 'id' );
+ var date = btn.data( 'date' );
+
+ $.confirm({
+ title: 'Potwierdzenie usunięcia',
+ content: 'Czy na pewno chcesz usunąć wpis z dnia ' + date + '?',
+ type: 'red',
+ buttons: {
+ confirm: {
+ text: 'Usuń',
+ btnClass: 'btn-red',
+ keys: ['enter'],
+ action: function()
+ {
+ $.ajax({
+ url: '/campaigns/delete_history_entry/history_id=' + history_id,
+ type: 'POST',
+ success: function( response )
+ {
+ var data = JSON.parse( response );
+ if ( data.success )
+ {
+ $.alert({
+ title: 'Sukces',
+ content: 'Wpis został usunięty.',
+ type: 'green',
+ autoClose: 'ok|2000'
+ });
+ $( '#products' ).DataTable().ajax.reload( null, false );
+ reloadChart();
+ }
+ else
+ {
+ $.alert({
+ title: 'Błąd',
+ content: data.message || 'Nie udało się usunąć wpisu.',
+ type: 'red'
+ });
+ }
+ }
+ });
+ }
+ },
+ cancel: { text: 'Anuluj' }
+ }
+ });
+ });
+
+ // Załaduj dane po wyborze kampanii
+ $( 'body' ).on( 'change', '#campaign_id', function()
+ {
+ var campaign_id = $( this ).val();
+
+ table = $( '#products' ).DataTable();
+ table.destroy();
+
+ new DataTable( '#products', {
+ ajax: {
+ type: 'POST',
+ url: '/campaigns/get_campaign_history_data_table/campaign_id=' + campaign_id,
+ },
+ processing: true,
+ serverSide: true,
+ searching: false,
+ lengthChange: false,
+ pageLength: 15,
+ columns: [
+ { width: '130px', name: 'date', orderable: false, className: "nowrap" },
+ { width: '120px', name: 'roas30', orderable: false, className: "dt-type-numeric" },
+ { width: '120px', name: 'roas_all_time', orderable: false, className: "dt-type-numeric" },
+ { width: '180px', name: 'conversion_value', orderable: false, className: "dt-type-numeric" },
+ { width: '140px', name: 'spend30', orderable: false, className: "dt-type-numeric" },
+ { width: 'auto', name: 'comment', orderable: false },
+ { width: 'auto', name: 'bidding_strategy', orderable: false },
+ { width: '100px', name: 'budget', orderable: false, className: "dt-type-numeric" },
+ { width: '60px', name: 'actions', orderable: false, className: "dt-center" }
+ ],
+ language: {
+ processing: 'Ładowanie...',
+ emptyTable: 'Brak danych do wyświetlenia',
+ info: 'Wpisy _START_ - _END_ z _TOTAL_',
+ infoEmpty: '',
+ lengthMenu: 'Pokaż _MENU_ wpisów',
+ paginate: {
+ first: 'Pierwsza',
+ last: 'Ostatnia',
+ next: 'Dalej',
+ previous: 'Wstecz'
+ }
+ }
+ });
+
+ reloadChart();
+ });
+});
+
diff --git a/templates/clients/main_view.php b/templates/clients/main_view.php
new file mode 100644
index 0000000..62c4aa5
--- /dev/null
+++ b/templates/clients/main_view.php
@@ -0,0 +1,158 @@
+
| #ID | +Nazwa klienta | +Google Ads Customer ID | +Dane od | +Akcje | +
|---|---|---|---|---|
| = $client['id']; ?> | += htmlspecialchars( $client['name'] ); ?> | ++ + = htmlspecialchars( $client['google_ads_customer_id'] ); ?> + + — brak — + + | ++ + = $client['google_ads_start_date']; ?> + + — brak — + + | ++ + + | +
|
+
+ Brak klientów. Dodaj pierwszego klienta. + |
+ ||||
| + | Id | +Id oferty | +Nazwa produktu | +Wyśw. | +Wyśw. (30d) | +Klik. | +Klik. (30d) | +CTR | +Koszt | +CPC | +Konw. | +Wart. konw. | +ROAS | +Min. ROAS | +CL3 | +CL4 | +Akcje | +
|---|
| - | Id | -Id oferty | -Nazwa produktu | -Wyśw. | -Wyśw. (30 dni) | -Klik. | -Klik. (30 dni) | -CTR | -Koszt | -CPC | -Konw. | -Wartość konw. | -ROAS | -Min. ROAS | -CL3 | -CL4 | -Akcje | -
|---|
System zarządzania reklamami
Google ADS & Facebook ADS