Add migrations for Google Ads settings and demo data
- Create migration for global settings table and add google_ads_customer_id and google_ads_start_date columns to clients table. - Add migration to include product_url column in products_data table. - Insert demo data for campaigns, products, and their history for client 'pomysloweprezenty.pl'. - Implement client management interface with modals for adding and editing clients, including Google Ads Customer ID and data retrieval start date.
This commit is contained in:
14
.claude/settings.local.json
Normal file
14
.claude/settings.local.json
Normal file
@@ -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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
16
.htaccess
16
.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]
|
||||
# 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]
|
||||
|
||||
4
.vscode/ftp-kr.sync.cache.json
vendored
4
.vscode/ftp-kr.sync.cache.json
vendored
@@ -161,8 +161,8 @@
|
||||
"products": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 19004,
|
||||
"lmtime": 1769727759481,
|
||||
"size": 19064,
|
||||
"lmtime": 1770756800564,
|
||||
"modified": false
|
||||
},
|
||||
"product_history.php": {
|
||||
|
||||
70
autoload/controls/class.Clients.php
Normal file
70
autoload/controls/class.Clients.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
namespace controls;
|
||||
|
||||
class Clients
|
||||
{
|
||||
static public function main_view()
|
||||
{
|
||||
return \view\Clients::main_view(
|
||||
\factory\Clients::get_all()
|
||||
);
|
||||
}
|
||||
|
||||
static public function save()
|
||||
{
|
||||
$id = \S::get( 'id' );
|
||||
$name = trim( \S::get( 'name' ) );
|
||||
$google_ads_customer_id = trim( \S::get( 'google_ads_customer_id' ) );
|
||||
|
||||
if ( !$name )
|
||||
{
|
||||
\S::alert( 'Nazwa klienta jest wymagana.' );
|
||||
header( 'Location: /clients' );
|
||||
exit;
|
||||
}
|
||||
|
||||
$google_ads_start_date = trim( \S::get( 'google_ads_start_date' ) );
|
||||
|
||||
$data = [
|
||||
'name' => $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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
'<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 . '">',
|
||||
'<a href="#" class="text-danger delete-product" product_id="' . $row['product_id'] . '">Usuń</a>'
|
||||
'<button type="button" class="btn btn-danger btn-sm delete-product" product_id="' . $row['product_id'] . '"><i class="fa-solid fa-trash"></i></button>'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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.' );
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
36
autoload/factory/class.Clients.php
Normal file
36
autoload/factory/class.Clients.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
namespace factory;
|
||||
|
||||
class Clients
|
||||
{
|
||||
static public function get_all()
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> 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 ] );
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
274
autoload/services/class.GoogleAdsApi.php
Normal file
274
autoload/services/class.GoogleAdsApi.php
Normal file
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
namespace services;
|
||||
|
||||
class GoogleAdsApi
|
||||
{
|
||||
private $developer_token;
|
||||
private $client_id;
|
||||
private $client_secret;
|
||||
private $refresh_token;
|
||||
private $manager_account_id;
|
||||
private $access_token;
|
||||
|
||||
private static $API_VERSION = 'v23';
|
||||
private static $TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
||||
private static $ADS_BASE_URL = 'https://googleads.googleapis.com';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this -> 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;
|
||||
}
|
||||
}
|
||||
282
autoload/services/class.OpenAiApi.php
Normal file
282
autoload/services/class.OpenAiApi.php
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
namespace services;
|
||||
class OpenAiApi
|
||||
{
|
||||
static private $api_url = 'https://api.openai.com/v1/chat/completions';
|
||||
|
||||
static private $system_prompt = 'Jesteś ekspertem od optymalizacji feedów produktowych Google Merchant Center i reklam Google Ads (Performance Max, Shopping).
|
||||
|
||||
OFICJALNE ZASADY GOOGLE MERCHANT CENTER — BEZWZGLĘDNIE PRZESTRZEGAJ:
|
||||
|
||||
TYTUŁY (title) — max 150 znaków:
|
||||
Zalecana struktura: [Marka] [Typ produktu] [Kluczowe atrybuty: kolor, rozmiar, materiał — TYLKO jeśli wynikają z nazwy]
|
||||
- Najważniejsze informacje umieść w pierwszych 70 znakach (reszta może być obcięta na mobile)
|
||||
- Każdy wariant produktu musi mieć unikalny tytuł (różne kolory/rozmiary = różne tytuły)
|
||||
- Tytuł MUSI być spójny z nazwą produktu na stronie docelowej
|
||||
- Używaj naturalnego języka — pisz dla ludzi, nie dla algorytmów
|
||||
ZAKAZANE w tytułach:
|
||||
- Tekst promocyjny: bestseller, hit, okazja, wyprzedaż, rabat, najlepszy, TOP, #1, idealny, polecamy, nowość
|
||||
- Wykrzykniki (!) i nadmierna interpunkcja
|
||||
- WIELKIE LITERY (wyjątek: ustalone skróty jak LED, USB, TV)
|
||||
- Emotikony, emoji, symbole dekoracyjne (★, ♥, ✓)
|
||||
- Informacje o cenie, dostawie, promocji
|
||||
- Wezwania do działania (kup teraz, sprawdź, zamów)
|
||||
- Upychanie słów kluczowych (keyword stuffing) — np. powtarzanie tych samych fraz
|
||||
- Kody wewnętrzne, SKU, numery katalogowe
|
||||
- Cechy produktu których NIE MA w oryginalnej nazwie (nie wymyślaj koloru, materiału, rozmiaru)
|
||||
|
||||
OPISY (description) — max 5000 znaków, ale najważniejsze info w pierwszych 160-500 znakach:
|
||||
- Opisz cechy, specyfikacje techniczne, zastosowanie, grupę docelową
|
||||
- Wymień atrybuty niewidoczne na zdjęciu (materiał, wzór, przeznaczenie wiekowe)
|
||||
- Ton neutralny, informacyjny — jak w katalogu produktowym
|
||||
ZAKAZANE w opisach:
|
||||
- Tekst promocyjny i reklamowy (te same słowa co w tytułach)
|
||||
- Nazwa firmy/sklepu (chyba że to marka produktu)
|
||||
- Informacje o wysyłce, cenach, promocjach
|
||||
- Wezwania do działania
|
||||
- Opisy innych produktów, akcesoriów nie wchodzących w skład oferty
|
||||
- Tekst zastępczy (lorem ipsum, placeholder)
|
||||
- Powielanie tego samego opisu dla wielu produktów
|
||||
- HTML, tagi formatowania
|
||||
|
||||
OGÓLNE:
|
||||
- Pisz poprawną polszczyzną (ortografia, gramatyka, interpunkcja)
|
||||
- Używaj pisowni zdaniowej lub tytułowej — NIE CAPS
|
||||
- Dane w feedzie MUSZĄ zgadzać się z danymi na stronie produktowej
|
||||
- Opisuj TYLKO cechy wynikające z nazwy produktu — nie wymyślaj
|
||||
|
||||
Twoje odpowiedzi muszą być:
|
||||
- Ściśle zgodne z polityką Google Merchant Center
|
||||
- Zgodne z prawdą (opisuj tylko cechy widoczne w nazwie produktu)
|
||||
- Zoptymalizowane pod kątem trafności wyszukiwań w Google Ads
|
||||
- W języku polskim';
|
||||
|
||||
static public function is_configured()
|
||||
{
|
||||
return (bool) GoogleAdsApi::get_setting( 'openai_api_key' );
|
||||
}
|
||||
|
||||
static public function fetch_page_content( $url )
|
||||
{
|
||||
$ch = curl_init( $url );
|
||||
curl_setopt_array( $ch, [
|
||||
CURLOPT_RETURNTRANSFER => 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( '/<script[^>]*>.*?<\/script>/si', '', $html );
|
||||
$html = preg_replace( '/<style[^>]*>.*?<\/style>/si', '', $html );
|
||||
$html = preg_replace( '/<!--.*?-->/s', '', $html );
|
||||
$html = preg_replace( '/<nav[^>]*>.*?<\/nav>/si', '', $html );
|
||||
$html = preg_replace( '/<footer[^>]*>.*?<\/footer>/si', '', $html );
|
||||
$html = preg_replace( '/<header[^>]*>.*?<\/header>/si', '', $html );
|
||||
|
||||
// Zamień tagi na spacje i wyciągnij tekst
|
||||
$text = strip_tags( $html );
|
||||
$text = html_entity_decode( $text, ENT_QUOTES, 'UTF-8' );
|
||||
$text = preg_replace( '/\s+/', ' ', $text );
|
||||
$text = trim( $text );
|
||||
|
||||
// Ogranicz do ~3000 znaków
|
||||
if ( mb_strlen( $text ) > 3000 )
|
||||
$text = mb_substr( $text, 0, 3000 ) . '...';
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
static private function call_api( $system_prompt, $user_prompt, $max_tokens = 500 )
|
||||
{
|
||||
$api_key = GoogleAdsApi::get_setting( 'openai_api_key' );
|
||||
$model = GoogleAdsApi::get_setting( 'openai_model' ) ?: 'gpt-5-mini';
|
||||
|
||||
// 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';
|
||||
|
||||
$payload = [
|
||||
'model' => $model,
|
||||
'messages' => [
|
||||
[ 'role' => 'system', 'content' => $system_prompt ],
|
||||
[ 'role' => 'user', 'content' => $user_prompt ]
|
||||
],
|
||||
'temperature' => 0.7,
|
||||
$tokens_key => $max_tokens
|
||||
];
|
||||
|
||||
$ch = curl_init( self::$api_url );
|
||||
curl_setopt_array( $ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $api_key
|
||||
],
|
||||
CURLOPT_POSTFIELDS => json_encode( $payload ),
|
||||
CURLOPT_TIMEOUT => 30
|
||||
] );
|
||||
|
||||
$response = curl_exec( $ch );
|
||||
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||
curl_close( $ch );
|
||||
|
||||
if ( $http_code !== 200 )
|
||||
{
|
||||
$error = json_decode( $response, true );
|
||||
return [ 'status' => 'error', 'message' => $error['error']['message'] ?? 'Błąd API OpenAI (HTTP ' . $http_code . ')' ];
|
||||
}
|
||||
|
||||
$data = json_decode( $response, true );
|
||||
$content = trim( $data['choices'][0]['message']['content'] ?? '' );
|
||||
|
||||
return [ 'status' => 'ok', 'suggestion' => $content ];
|
||||
}
|
||||
|
||||
static private function build_context_text( $context )
|
||||
{
|
||||
$lines = [];
|
||||
$lines[] = 'Nazwa produktu: ' . ( $context['original_name'] ?? '—' );
|
||||
|
||||
if ( !empty( $context['current_title'] ) )
|
||||
$lines[] = 'Obecny tytuł (custom): ' . $context['current_title'];
|
||||
|
||||
if ( !empty( $context['current_description'] ) )
|
||||
$lines[] = 'Obecny opis: ' . $context['current_description'];
|
||||
|
||||
if ( !empty( $context['current_category'] ) )
|
||||
$lines[] = 'Obecna kategoria Google: ' . $context['current_category'];
|
||||
|
||||
if ( !empty( $context['offer_id'] ) )
|
||||
$lines[] = 'ID oferty: ' . $context['offer_id'];
|
||||
|
||||
if ( !empty( $context['custom_label_4'] ) )
|
||||
$lines[] = 'Status produktu: ' . $context['custom_label_4'];
|
||||
|
||||
$lines[] = '';
|
||||
$lines[] = 'Metryki reklamowe (ostatnie 30 dni):';
|
||||
$lines[] = '- Wyświetlenia: ' . ( $context['impressions_30'] ?? 0 );
|
||||
$lines[] = '- Kliknięcia: ' . ( $context['clicks_30'] ?? 0 );
|
||||
$lines[] = '- CTR: ' . ( $context['ctr'] ?? 0 ) . '%';
|
||||
$lines[] = '- Koszt: ' . ( $context['cost'] ?? 0 ) . ' PLN';
|
||||
$lines[] = '- Konwersje: ' . ( $context['conversions'] ?? 0 );
|
||||
$lines[] = '- Wartość konwersji: ' . ( $context['conversions_value'] ?? 0 ) . ' PLN';
|
||||
$lines[] = '- ROAS: ' . ( $context['roas'] ?? 0 ) . '%';
|
||||
|
||||
if ( !empty( $context['page_content'] ) )
|
||||
{
|
||||
$lines[] = '';
|
||||
$lines[] = 'Treść ze strony produktu (użyj tych informacji do stworzenia dokładniejszego opisu):';
|
||||
$lines[] = $context['page_content'];
|
||||
}
|
||||
|
||||
return implode( "\n", $lines );
|
||||
}
|
||||
|
||||
static public function suggest_title( $context )
|
||||
{
|
||||
$context_text = self::build_context_text( $context );
|
||||
|
||||
$prompt = 'Zaproponuj zoptymalizowany tytuł produktu dla Google Merchant Center.
|
||||
|
||||
WYMAGANIA:
|
||||
- Max 150 znaków, najważniejsze info w pierwszych 70 znakach
|
||||
- Struktura: [Marka jeśli jest w nazwie] [Typ produktu] [Kluczowe cechy z nazwy] [Dla kogo/okazja jeśli wynika z nazwy]
|
||||
- Pisownia zdaniowa lub tytułowa, naturalny język
|
||||
- Tytuł musi brzmieć jak wpis w katalogu produktowym
|
||||
|
||||
BEZWZGLĘDNY ZAKAZ (odrzucenie przez Google):
|
||||
- Słowa promocyjne: bestseller, hit, idealny, najlepszy, polecamy, okazja, nowość, TOP
|
||||
- Wykrzykniki (!), emotikony, symbole dekoracyjne
|
||||
- WIELKIE LITERY (wyjątek: LED, USB, TV)
|
||||
- Wezwania do działania, informacje o cenie/dostawie
|
||||
- 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 . '
|
||||
|
||||
Zwróć TYLKO tytuł, bez cudzysłowów, bez wyjaśnień.';
|
||||
|
||||
return self::call_api( self::$system_prompt, $prompt );
|
||||
}
|
||||
|
||||
static public function suggest_description( $context )
|
||||
{
|
||||
$context_text = self::build_context_text( $context );
|
||||
$has_page = !empty( $context['page_content'] );
|
||||
|
||||
$length_guide = $has_page
|
||||
? '- Napisz rozbudowany, szczegółowy opis: ok. 1000 znaków (800-1200)
|
||||
- Wykorzystaj szczegóły ze strony produktu: skład zestawu, materiały, wymiary, kolory, przeznaczenie
|
||||
- Każdy akapit/punkt powinien wnosić NOWĄ informację — NIE powtarzaj tych samych elementów'
|
||||
: '- Napisz opis o długości ok. 1000 znaków (800-1200) — jeśli brak szczegółów, opisz ogólnie zastosowanie i grupę docelową
|
||||
- Opisuj TYLKO to co wynika z oryginalnej nazwy — nie wymyślaj konkretnych parametrów';
|
||||
|
||||
$prompt = 'Napisz zoptymalizowany opis produktu dla Google Merchant Center.
|
||||
|
||||
WYMAGANIA:
|
||||
' . $length_guide . '
|
||||
- Najważniejsze info na początku (pierwsze 160 znaków widoczne w wynikach)
|
||||
- Rzeczowo opisz: co to jest, z czego się składa, do czego służy, dla kogo
|
||||
- Ton neutralny, informacyjny — jak w specyfikacji produktowej
|
||||
- Każdy akapit musi zawierać INNĄ informację niż poprzedni — NIGDY nie powtarzaj tych samych elementów
|
||||
|
||||
FORMATOWANIE — Google Merchant Center obsługuje podstawowe HTML:
|
||||
- Używaj <br> do oddzielania akapitów/sekcji
|
||||
- Używaj <b>pogrubienia</b> dla kluczowych cech (np. nazwy elementów zestawu, materiał)
|
||||
- Używaj <ul><li>...</li></ul> gdy wymieniasz elementy zestawu lub listę cech
|
||||
- NIE używaj innych tagów HTML (h1, p, div, span, img, a, table itp.)
|
||||
- NIE używaj Markdown — tylko dozwolone tagi HTML
|
||||
|
||||
BEZWZGLĘDNY ZAKAZ (odrzucenie przez Google):
|
||||
- Słowa promocyjne: bestseller, hit, okazja, najlepszy, polecamy, nowość, idealny
|
||||
- Wykrzykniki (!), emotikony, WIELKIE LITERY
|
||||
- Wezwania do działania: kup teraz, zamów, sprawdź, dodaj do koszyka
|
||||
- Informacje o cenie, dostawie, promocjach, nazwie sklepu
|
||||
- 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 . '
|
||||
|
||||
Zwróć TYLKO opis w formacie HTML (używając dozwolonych tagów), bez cudzysłowów, bez wyjaśnień.';
|
||||
|
||||
$tokens = $has_page ? 1500 : 1000;
|
||||
return self::call_api( self::$system_prompt, $prompt, $tokens );
|
||||
}
|
||||
|
||||
static public function suggest_category( $context, $categories = [] )
|
||||
{
|
||||
$context_text = self::build_context_text( $context );
|
||||
|
||||
$cats_text = '';
|
||||
if ( !empty( $categories ) )
|
||||
{
|
||||
$cats_list = array_slice( $categories, 0, 50 );
|
||||
$cats_text = "\n\nDostępne kategorie Google Product Taxonomy (format: id - tekst):\n";
|
||||
foreach ( $cats_list as $cat )
|
||||
{
|
||||
$cats_text .= $cat['id'] . ' - ' . $cat['text'] . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
$prompt = 'Dopasuj produkt do najlepszej kategorii Google Product Taxonomy.
|
||||
Wybierz najbardziej szczegółową (najgłębszą) kategorię pasującą do produktu.
|
||||
|
||||
' . $context_text . $cats_text . '
|
||||
|
||||
Zwróć TYLKO ID kategorii (liczbę), bez wyjaśnień.';
|
||||
|
||||
return self::call_api( self::$system_prompt, $prompt );
|
||||
}
|
||||
}
|
||||
12
autoload/view/class.Clients.php
Normal file
12
autoload/view/class.Clients.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace view;
|
||||
|
||||
class Clients
|
||||
{
|
||||
static public function main_view( $clients )
|
||||
{
|
||||
return \Tpl::view( 'clients/main_view', [
|
||||
'clients' => $clients,
|
||||
] );
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,17 @@ class Site
|
||||
{
|
||||
public static function show()
|
||||
{
|
||||
global $user;
|
||||
|
||||
$class = '\controls\\';
|
||||
|
||||
$results = explode( '_', \S::get( 'module' ) );
|
||||
if ( is_array( $results ) ) foreach ( $results as $row )
|
||||
$class .= ucfirst( $row );
|
||||
|
||||
$action = \S::get( 'action' );
|
||||
global $user, $current_module;
|
||||
|
||||
$tpl = new \Tpl;
|
||||
$tpl -> content = \controls\Site::route();
|
||||
|
||||
if ( !$user )
|
||||
if ( !$user )
|
||||
return $tpl -> render( 'site/layout-unlogged' );
|
||||
else
|
||||
{
|
||||
$tpl -> user = $user;
|
||||
$tpl -> current_module = $current_module;
|
||||
if ( $alert = \S::get_session( 'alert' ) )
|
||||
{
|
||||
$tpl -> alert = $alert;
|
||||
|
||||
280
docs/PLAN.md
Normal file
280
docs/PLAN.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# adsPRO - System Zarządzania Reklamami Google ADS & Facebook ADS
|
||||
|
||||
## Opis projektu
|
||||
adsPRO to narzędzie webowe (PHP) do zarządzania i automatyzacji kampanii reklamowych Google ADS (priorytet) oraz Facebook ADS (planowane). System umożliwia monitorowanie kampanii, zarządzanie produktami, analizę wydajności (ROAS, CTR, CPC) oraz automatyczne etykietowanie produktów (bestsellery, zombie itp.).
|
||||
|
||||
**URL:** https://adspro.projectpro.pl
|
||||
**Hosting:** Hostido (shared hosting)
|
||||
|
||||
## Stack technologiczny
|
||||
- **PHP 8.x** - czyste PHP z własną strukturą MVC (bez frameworka)
|
||||
- **MySQL/MariaDB** - baza danych (Medoo ORM)
|
||||
- **Google ADS API** - pobieranie danych kampanii i produktów (CRON)
|
||||
- **Facebook ADS API** - planowane w przyszłości
|
||||
- **CRON** - automatyczna synchronizacja danych
|
||||
- **SCSS** - stylowanie (kompilacja do CSS)
|
||||
- **jQuery 3.6** - interaktywność frontend
|
||||
- **DataTables 2.1.7** - tabele z sortowaniem, filtrowaniem, paginacją
|
||||
- **Highcharts** - wykresy wydajności
|
||||
- **Select2** - zaawansowane selecty
|
||||
- **Font Awesome** - ikony
|
||||
|
||||
## Struktura katalogów (nowa)
|
||||
|
||||
```
|
||||
public_html/
|
||||
├── index.php # Front controller + nowy router
|
||||
├── .htaccess # Rewrite rules
|
||||
├── .env # Konfiguracja (przyszłość - migracja z config.php)
|
||||
├── config.php # Konfiguracja DB (obecna)
|
||||
├── ajax.php # Ajax handler
|
||||
├── api.php # API handler (Google ADS webhook)
|
||||
├── cron.php # CRON handler
|
||||
├── robots.txt
|
||||
├── layout/
|
||||
│ ├── favicon.png
|
||||
│ ├── style.scss # Główne style (SCSS)
|
||||
│ └── style.css # Skompilowane style
|
||||
├── libraries/ # Biblioteki zewnętrzne
|
||||
│ ├── medoo/
|
||||
│ ├── phpmailer/
|
||||
│ ├── select2/
|
||||
│ ├── jquery-confirm/
|
||||
│ ├── functions.js # Globalne funkcje JS
|
||||
│ └── framework/ # Framework UI (skin, pluginy)
|
||||
├── autoload/ # Kod PHP (MVC)
|
||||
│ ├── class.S.php # Helper: sesje, requesty, narzędzia
|
||||
│ ├── class.Tpl.php # Template engine
|
||||
│ ├── class.Cache.php # Cache
|
||||
│ ├── class.DbModel.php # Bazowy model DB
|
||||
│ ├── class.Html.php # HTML helper
|
||||
│ ├── controls/ # Kontrolery
|
||||
│ │ ├── class.Site.php # Router (do przebudowy)
|
||||
│ │ ├── class.Users.php # Logowanie, ustawienia
|
||||
│ │ ├── class.Dashboard.php # NOWY - Dashboard główny
|
||||
│ │ ├── class.Campaigns.php # Kampanie Google ADS
|
||||
│ │ ├── class.Products.php # Produkty
|
||||
│ │ ├── class.Allegro.php # Import Allegro
|
||||
│ │ ├── class.Reports.php # NOWY - Raporty i analityka
|
||||
│ │ ├── class.Api.php # Endpointy API
|
||||
│ │ └── class.Cron.php # CRON joby
|
||||
│ ├── factory/ # Modele danych (DB queries)
|
||||
│ │ ├── class.Users.php
|
||||
│ │ ├── class.Campaigns.php
|
||||
│ │ ├── class.Products.php
|
||||
│ │ └── class.Cron.php
|
||||
│ └── view/ # View helpers
|
||||
│ ├── class.Site.php # Renderer layoutu
|
||||
│ ├── class.Users.php
|
||||
│ └── class.Cron.php
|
||||
├── templates/ # Szablony PHP
|
||||
│ ├── site/
|
||||
│ │ ├── layout-logged.php # Layout z sidebar (PRZEBUDOWA)
|
||||
│ │ └── layout-unlogged.php # Layout logowania (PRZEBUDOWA)
|
||||
│ ├── auth/
|
||||
│ │ └── login.php # NOWY ekran logowania
|
||||
│ ├── dashboard/
|
||||
│ │ └── index.php # NOWY dashboard
|
||||
│ ├── campaigns/
|
||||
│ │ └── main_view.php # Widok kampanii
|
||||
│ ├── products/
|
||||
│ │ ├── main_view.php # Lista produktów
|
||||
│ │ └── product_history.php # Historia produktu
|
||||
│ ├── allegro/
|
||||
│ │ └── main_view.php # Import Allegro
|
||||
│ ├── reports/ # NOWE
|
||||
│ │ └── index.php # Raporty
|
||||
│ ├── users/
|
||||
│ │ ├── login-form.php # Stary login (do usunięcia)
|
||||
│ │ └── settings.php # Ustawienia użytkownika
|
||||
│ └── html/ # Komponenty HTML
|
||||
│ ├── button.php
|
||||
│ ├── input.php
|
||||
│ ├── select.php
|
||||
│ └── ...
|
||||
├── tools/
|
||||
│ └── google-taxonomy.php
|
||||
├── tmp/
|
||||
└── docs/
|
||||
└── PLAN.md
|
||||
```
|
||||
|
||||
## Nowy system routingu
|
||||
|
||||
### Zasada działania
|
||||
Zamiast obecnego `?module=X&action=Y` → czyste URLe obsługiwane przez `.htaccess` + nowy router w `class.Site.php`.
|
||||
|
||||
### Mapa URL
|
||||
|
||||
| URL | Kontroler | Metoda | Opis |
|
||||
|-----|-----------|--------|------|
|
||||
| `/login` | Users | login_form | Ekran logowania |
|
||||
| `/logout` | Users | logout | Wylogowanie |
|
||||
| `/` | Dashboard | index | Dashboard główny |
|
||||
| `/campaigns` | Campaigns | main_view | Lista kampanii |
|
||||
| `/campaigns/history/{id}` | Campaigns | history | Historia kampanii |
|
||||
| `/products` | Products | main_view | Lista produktów |
|
||||
| `/products/history/{id}` | Products | product_history | Historia produktu |
|
||||
| `/allegro` | Allegro | main_view | Import Allegro |
|
||||
| `/reports` | Reports | index | Raporty |
|
||||
| `/settings` | Users | settings | Ustawienia konta |
|
||||
| `/api/*` | Api | * | Endpointy API |
|
||||
| `/cron/*` | Cron | * | CRON joby |
|
||||
|
||||
### Nowy .htaccess
|
||||
```apache
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
|
||||
# Statyczne zasoby - pomijaj
|
||||
RewriteCond %{REQUEST_URI} ^/(libraries|layout|upload|temp)/ [NC]
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# Wszystko inne → index.php
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*)$ index.php [L,QSA]
|
||||
```
|
||||
|
||||
### Nowy router (class.Site.php)
|
||||
```php
|
||||
// Parsowanie URL z $_SERVER['REQUEST_URI']
|
||||
// Mapowanie: /segment1/segment2/segment3 → kontroler/akcja/parametry
|
||||
// Fallback na dashboard dla zalogowanych, login dla niezalogowanych
|
||||
```
|
||||
|
||||
## Główne funkcje
|
||||
|
||||
### 1. Nowy ekran logowania
|
||||
- Nowoczesny design: podzielony ekran (lewa strona - branding/grafika, prawa - formularz)
|
||||
- Logo "adsPRO" z subtitlem
|
||||
- Pola: email + hasło
|
||||
- Checkbox "Zapamiętaj mnie"
|
||||
- Walidacja AJAX
|
||||
- Animacje przejścia
|
||||
- Responsywność (mobile: tylko formularz)
|
||||
|
||||
### 2. Nowy layout z menu bocznym (sidebar)
|
||||
- **Sidebar (lewa strona, 260px):**
|
||||
- Logo "adsPRO" na górze
|
||||
- Menu nawigacyjne z ikonami Font Awesome:
|
||||
- 📊 Dashboard (`/`)
|
||||
- 📢 Kampanie (`/campaigns`)
|
||||
- 📦 Produkty (`/products`)
|
||||
- 📥 Allegro import (`/allegro`)
|
||||
- 📈 Raporty (`/reports`)
|
||||
- ⚙️ Ustawienia (`/settings`)
|
||||
- Aktywny element podświetlony
|
||||
- Możliwość zwijania sidebar (collapsed → same ikony, 60px)
|
||||
- Na dole: info o zalogowanym użytkowniku + przycisk wylogowania
|
||||
- **Top bar (nad contentem):**
|
||||
- Przycisk hamburger (toggle sidebar)
|
||||
- Breadcrumbs (ścieżka nawigacji)
|
||||
- Szybkie akcje / notyfikacje (przyszłość)
|
||||
- **Content area:**
|
||||
- Pełna szerokość minus sidebar
|
||||
- Padding 25px
|
||||
- Tło #F4F6F9 (jaśniejsze od obecnego)
|
||||
|
||||
### 3. Dashboard (NOWY)
|
||||
- Kafelki podsumowujące (karty):
|
||||
- Łączna liczba kampanii
|
||||
- Łączna liczba produktów
|
||||
- Średni ROAS (30 dni)
|
||||
- Łączne wydatki (30 dni)
|
||||
- Wykres trendu ROAS (ostatnie 30 dni)
|
||||
- Lista ostatnio zmodyfikowanych kampanii
|
||||
- Produkty wymagające uwagi (niski ROAS, zombie)
|
||||
|
||||
### 4. Zarządzanie kampaniami Google ADS
|
||||
- Wybór klienta (select)
|
||||
- Lista kampanii z metrykami (DataTables)
|
||||
- Historia kampanii z wykresem Highcharts
|
||||
- Metryki: ROAS, budżet, wydatki, wartość konwersji, strategia bidding
|
||||
- Usuwanie kampanii i wpisów historii
|
||||
- Komentarze do kampanii
|
||||
|
||||
### 5. Zarządzanie produktami
|
||||
- Wybór klienta
|
||||
- Konfiguracja min. ROAS dla bestsellerów
|
||||
- Tabela produktów z metrykami:
|
||||
- Wyświetlenia, kliknięcia, CTR, koszt, CPC
|
||||
- Konwersje, wartość konwersji, ROAS
|
||||
- Custom labels (bestseller/zombie/deleted/pla/paused)
|
||||
- Edycja inline (min_roas, custom_label)
|
||||
- Edycja produktu w modalu (tytuł, opis, kategoria Google)
|
||||
- Historia produktu z wykresem
|
||||
- Bulk delete zaznaczonych produktów
|
||||
|
||||
### 6. Import Allegro
|
||||
- Upload pliku CSV
|
||||
- Automatyczne mapowanie ofert
|
||||
- Raport importu (dodane, zaktualizowane)
|
||||
|
||||
### 7. Raporty (NOWY - przyszłość)
|
||||
- Raport wydajności kampanii
|
||||
- Raport produktów (bestsellery vs zombie)
|
||||
- Eksport do Excel
|
||||
- Porównanie okresów
|
||||
|
||||
### 8. Ustawienia
|
||||
- Dane konta (email)
|
||||
- Zmiana hasła
|
||||
- Konfiguracja Pushover (powiadomienia)
|
||||
- Klucze API (przyszłość: Google ADS, Facebook ADS)
|
||||
|
||||
### 9. CRON - synchronizacja danych
|
||||
- `cron_products` - synchronizacja produktów z Google ADS
|
||||
- `cron_products_history_30` - historia 30-dniowa produktów
|
||||
- `cron_xml` - generowanie XML
|
||||
- `cron_phrases` - synchronizacja fraz
|
||||
- `cron_phrases_history_30` - historia 30-dniowa fraz
|
||||
|
||||
## Plan implementacji
|
||||
|
||||
| Etap | Zakres | Priorytet | Pliki |
|
||||
|------|--------|-----------|-------|
|
||||
| **1. Nowy routing** | Przebudowa routera, nowy .htaccess, parsowanie czystych URL | 🔴 Wysoki | `.htaccess`, `index.php`, `controls/class.Site.php` |
|
||||
| **2. Nowy layout (sidebar)** | Layout z bocznym menu, top bar, responsywność | 🔴 Wysoki | `templates/site/layout-logged.php`, `layout/style.scss` |
|
||||
| **3. Nowy ekran logowania** | Nowoczesny split-screen login, nowy layout-unlogged | 🔴 Wysoki | `templates/site/layout-unlogged.php`, `templates/auth/login.php`, `layout/style.scss` |
|
||||
| **4. Dashboard** | Nowa strona startowa z podsumowaniem | 🟡 Średni | `controls/class.Dashboard.php`, `templates/dashboard/index.php` |
|
||||
| **5. Migracja kampanii** | Dostosowanie widoku kampanii do nowego routingu | 🟡 Średni | `controls/class.Campaigns.php`, `templates/campaigns/*` |
|
||||
| **6. Migracja produktów** | Dostosowanie widoku produktów do nowego routingu | 🟡 Średni | `controls/class.Products.php`, `templates/products/*` |
|
||||
| **7. Migracja Allegro** | Dostosowanie importu Allegro | 🟢 Niski | `controls/class.Allegro.php`, `templates/allegro/*` |
|
||||
| **8. Moduł raportów** | Nowy moduł analityczny | 🟢 Niski | `controls/class.Reports.php`, `templates/reports/*` |
|
||||
| **9. Facebook ADS** | Integracja z Facebook ADS API | 🔵 Przyszłość | Nowe kontrolery, factory, szablony |
|
||||
|
||||
## Kolorystyka i design
|
||||
|
||||
### Paleta kolorów
|
||||
- **Primary (akcent):** `#6690F4` (niebieski - obecny)
|
||||
- **Sidebar tło:** `#1E2A3A` (ciemny granat)
|
||||
- **Sidebar tekst:** `#A8B7C7` (jasny szary)
|
||||
- **Sidebar active:** `#6690F4` (primary)
|
||||
- **Content tło:** `#F4F6F9` (jasnoszary)
|
||||
- **Karty:** `#FFFFFF`
|
||||
- **Tekst:** `#4E5E6A` (obecny)
|
||||
- **Success:** `#57B951`
|
||||
- **Danger:** `#CC0000`
|
||||
- **Warning:** `#FF8C00`
|
||||
|
||||
### Typografia
|
||||
- Font: Open Sans (obecny - zachowany)
|
||||
- Rozmiar bazowy: 14px (sidebar), 15px (content)
|
||||
|
||||
## Bezpieczeństwo
|
||||
- Hasła hashowane MD5 (obecne) → **TODO: migracja na bcrypt**
|
||||
- Sesje PHP + cookie "zapamiętaj mnie"
|
||||
- Prepared statements (Medoo ORM)
|
||||
- htmlspecialchars() w szablonach
|
||||
- **TODO: CSRF tokeny w formularzach**
|
||||
- **TODO: migracja config.php → .env (z .htaccess deny)**
|
||||
|
||||
## Przyszłe rozszerzenia
|
||||
- Facebook ADS API - zarządzanie kampaniami FB
|
||||
- System powiadomień (Pushover + in-app)
|
||||
- Wielojęzyczność (PL/EN)
|
||||
- Role użytkowników (admin, manager, viewer)
|
||||
- Automatyczne reguły (np. "jeśli ROAS < X → zmień label na zombie")
|
||||
- Integracja z Google Merchant Center
|
||||
- API REST do integracji z innymi systemami
|
||||
153
docs/google_ads_api_design_doc.doc
Normal file
153
docs/google_ads_api_design_doc.doc
Normal file
@@ -0,0 +1,153 @@
|
||||
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Calibri, sans-serif; font-size: 11pt; color: #333; line-height: 1.6; margin: 40px; }
|
||||
h1 { font-size: 20pt; color: #1a73e8; border-bottom: 2px solid #1a73e8; padding-bottom: 8px; }
|
||||
h2 { font-size: 14pt; color: #1a73e8; margin-top: 24px; }
|
||||
h3 { font-size: 12pt; color: #444; margin-top: 16px; }
|
||||
p { margin: 6px 0; }
|
||||
ul { margin: 6px 0 6px 20px; }
|
||||
li { margin: 4px 0; }
|
||||
strong { color: #222; }
|
||||
code { font-family: Consolas, monospace; font-size: 10pt; background: #f5f5f5; padding: 2px 4px; }
|
||||
pre { font-family: Consolas, monospace; font-size: 9.5pt; background: #f8f8f8; border: 1px solid #ddd; padding: 12px; margin: 10px 0; white-space: pre-wrap; }
|
||||
hr { border: none; border-top: 1px solid #ccc; margin: 16px 0; }
|
||||
.meta { color: #666; font-size: 10pt; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>adsPRO — Google Ads API Tool Design Documentation</h1>
|
||||
|
||||
<p class="meta"><strong>Company:</strong> Project-Pro<br>
|
||||
<strong>Tool Name:</strong> adsPRO<br>
|
||||
<strong>Date:</strong> February 2026<br>
|
||||
<strong>Version:</strong> 1.0</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>1. Tool Overview</h2>
|
||||
|
||||
<p>adsPRO is an internal advertising management platform built for our agency to centralize and automate the management of Google Ads campaigns across multiple client accounts. The tool provides a unified dashboard for monitoring campaign performance, analyzing key metrics (ROAS, cost, conversion value), and managing campaign settings — eliminating the need to switch between multiple Google Ads accounts manually.</p>
|
||||
|
||||
<h2>2. Purpose and Business Use Case</h2>
|
||||
|
||||
<p>Our agency manages Google Ads campaigns for multiple clients. Currently, campaign data is collected manually or via Google Apps Script, which is fragile and hard to maintain. adsPRO replaces this workflow with a direct, reliable integration with the Google Ads API.</p>
|
||||
|
||||
<p><strong>Key goals:</strong></p>
|
||||
<ul>
|
||||
<li>Automatically retrieve campaign performance data for all managed client accounts</li>
|
||||
<li>Display historical trends (ROAS, budget, spend, conversions) in a centralized dashboard</li>
|
||||
<li>Enable campaign management operations (budget adjustments, bidding strategy changes, campaign status updates) directly from the platform</li>
|
||||
<li>Reduce manual work and improve response time for campaign optimization</li>
|
||||
</ul>
|
||||
|
||||
<h2>3. Google Ads API Usage</h2>
|
||||
|
||||
<h3>3.1 Data Retrieval (Read Operations)</h3>
|
||||
|
||||
<p>The tool uses the <strong>GoogleAdsService.SearchStream</strong> endpoint to fetch campaign data using GAQL (Google Ads Query Language).</p>
|
||||
|
||||
<p><strong>Data retrieved:</strong></p>
|
||||
<ul>
|
||||
<li>Campaign name, ID, and status</li>
|
||||
<li>Bidding strategy type and target ROAS</li>
|
||||
<li>Campaign budget (daily)</li>
|
||||
<li>Cost (spend) over the last 30 days</li>
|
||||
<li>Conversion value over the last 30 days</li>
|
||||
<li>All-time ROAS calculation</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>GAQL queries used:</strong></p>
|
||||
|
||||
<pre>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</pre>
|
||||
|
||||
<pre>SELECT campaign.id, metrics.cost_micros, metrics.conversions_value
|
||||
FROM campaign
|
||||
WHERE campaign.status = 'ENABLED'</pre>
|
||||
|
||||
<h3>3.2 Campaign Management (Write Operations)</h3>
|
||||
|
||||
<p>The tool will also support campaign management operations through the Google Ads API:</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Budget adjustments</strong> — updating <code>campaign_budget.amount_micros</code> via <code>CampaignBudgetService.MutateCampaignBudgets</code></li>
|
||||
<li><strong>Bidding strategy changes</strong> — modifying bidding strategy type and target ROAS via <code>CampaignService.MutateCampaigns</code></li>
|
||||
<li><strong>Campaign status updates</strong> — enabling/pausing campaigns via <code>CampaignService.MutateCampaigns</code></li>
|
||||
</ul>
|
||||
|
||||
<p>All write operations are initiated manually by authorized agency staff through the adsPRO interface. No automated modifications are made without human approval.</p>
|
||||
|
||||
<h2>4. API Request Frequency</h2>
|
||||
|
||||
<ul>
|
||||
<li><strong>Automated data retrieval:</strong> Once per day via server-side CRON job (scheduled at 06:00 CET)</li>
|
||||
<li><strong>Campaign management operations:</strong> On-demand, initiated manually by agency staff (estimated 10–50 requests per day)</li>
|
||||
<li><strong>Number of client accounts:</strong> Currently under 20, expected to grow to ~50</li>
|
||||
<li><strong>Estimated total daily API calls:</strong> Under 200 requests per day</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. Authentication and Authorization</h2>
|
||||
|
||||
<ul>
|
||||
<li>OAuth 2.0 authentication with offline access (refresh token flow)</li>
|
||||
<li>Credentials stored securely in a server-side database (not exposed to end users)</li>
|
||||
<li>Access token is refreshed automatically when expired</li>
|
||||
<li>Optional Manager Account (MCC) support for centralized access to client accounts</li>
|
||||
</ul>
|
||||
|
||||
<h2>6. Architecture</h2>
|
||||
|
||||
<pre>[CRON - daily at 06:00]
|
||||
|
|
||||
v
|
||||
[adsPRO Server (PHP)]
|
||||
|
|
||||
v
|
||||
[Google Ads REST API v18]
|
||||
- SearchStream (read campaign data)
|
||||
- MutateCampaigns (manage campaigns)
|
||||
- MutateCampaignBudgets (adjust budgets)
|
||||
|
|
||||
v
|
||||
[MySQL Database]
|
||||
- campaigns table (current state)
|
||||
- campaigns_history table (daily snapshots)
|
||||
|
|
||||
v
|
||||
[adsPRO Dashboard]
|
||||
- Campaign performance charts
|
||||
- ROAS tracking over time
|
||||
- Campaign management interface</pre>
|
||||
|
||||
<h2>7. Data Handling and Privacy</h2>
|
||||
|
||||
<ul>
|
||||
<li>All data is stored on our own private server (shared hosting with SSL)</li>
|
||||
<li>Data is accessible only to authenticated agency staff (login required)</li>
|
||||
<li>No campaign data is shared with third parties</li>
|
||||
<li>No personally identifiable information (PII) is collected from end users</li>
|
||||
<li>API credentials are stored server-side and never exposed in client-facing code</li>
|
||||
</ul>
|
||||
|
||||
<h2>8. Users</h2>
|
||||
|
||||
<p>This is an <strong>internal agency tool</strong>. It is not a publicly available application. Access is restricted to authorized staff members of our agency (currently 3 users). There is no self-registration — accounts are created by the administrator.</p>
|
||||
|
||||
<h2>9. Compliance</h2>
|
||||
|
||||
<ul>
|
||||
<li>The tool complies with the Google Ads API Terms of Service</li>
|
||||
<li>The tool complies with the Google API Services User Data Policy</li>
|
||||
<li>No data is sold or shared with third parties</li>
|
||||
<li>The tool does not perform automated campaign modifications without human oversight</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
122
docs/google_ads_api_design_doc.md
Normal file
122
docs/google_ads_api_design_doc.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# adsPRO — Google Ads API Tool Design Documentation
|
||||
|
||||
**Company:** Project-Pro
|
||||
**Tool Name:** adsPRO
|
||||
**Date:** February 2026
|
||||
**Version:** 1.0
|
||||
|
||||
---
|
||||
|
||||
## 1. Tool Overview
|
||||
|
||||
adsPRO is an internal advertising management platform built for our agency to centralize and automate the management of Google Ads campaigns across multiple client accounts. The tool provides a unified dashboard for monitoring campaign performance, analyzing key metrics (ROAS, cost, conversion value), and managing campaign settings — eliminating the need to switch between multiple Google Ads accounts manually.
|
||||
|
||||
## 2. Purpose and Business Use Case
|
||||
|
||||
Our agency manages Google Ads campaigns for multiple clients. Currently, campaign data is collected manually or via Google Apps Script, which is fragile and hard to maintain. adsPRO replaces this workflow with a direct, reliable integration with the Google Ads API.
|
||||
|
||||
**Key goals:**
|
||||
- Automatically retrieve campaign performance data for all managed client accounts
|
||||
- Display historical trends (ROAS, budget, spend, conversions) in a centralized dashboard
|
||||
- Enable campaign management operations (budget adjustments, bidding strategy changes, campaign status updates) directly from the platform
|
||||
- Reduce manual work and improve response time for campaign optimization
|
||||
|
||||
## 3. Google Ads API Usage
|
||||
|
||||
### 3.1 Data Retrieval (Read Operations)
|
||||
|
||||
The tool uses the **GoogleAdsService.SearchStream** endpoint to fetch campaign data using GAQL (Google Ads Query Language).
|
||||
|
||||
**Data retrieved:**
|
||||
- Campaign name, ID, and status
|
||||
- Bidding strategy type and target ROAS
|
||||
- Campaign budget (daily)
|
||||
- Cost (spend) over the last 30 days
|
||||
- Conversion value over the last 30 days
|
||||
- All-time ROAS calculation
|
||||
|
||||
**GAQL queries used:**
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
```
|
||||
SELECT campaign.id, metrics.cost_micros, metrics.conversions_value
|
||||
FROM campaign
|
||||
WHERE campaign.status = 'ENABLED'
|
||||
```
|
||||
|
||||
### 3.2 Campaign Management (Write Operations)
|
||||
|
||||
The tool will also support campaign management operations through the Google Ads API:
|
||||
|
||||
- **Budget adjustments** — updating `campaign_budget.amount_micros` via `CampaignBudgetService.MutateCampaignBudgets`
|
||||
- **Bidding strategy changes** — modifying bidding strategy type and target ROAS via `CampaignService.MutateCampaigns`
|
||||
- **Campaign status updates** — enabling/pausing campaigns via `CampaignService.MutateCampaigns`
|
||||
|
||||
All write operations are initiated manually by authorized agency staff through the adsPRO interface. No automated modifications are made without human approval.
|
||||
|
||||
## 4. API Request Frequency
|
||||
|
||||
- **Automated data retrieval:** Once per day via server-side CRON job (scheduled at 06:00 CET)
|
||||
- **Campaign management operations:** On-demand, initiated manually by agency staff (estimated 10–50 requests per day)
|
||||
- **Number of client accounts:** Currently under 20, expected to grow to ~50
|
||||
- **Estimated total daily API calls:** Under 200 requests per day
|
||||
|
||||
## 5. Authentication and Authorization
|
||||
|
||||
- OAuth 2.0 authentication with offline access (refresh token flow)
|
||||
- Credentials stored securely in a server-side database (not exposed to end users)
|
||||
- Access token is refreshed automatically when expired
|
||||
- Optional Manager Account (MCC) support for centralized access to client accounts
|
||||
|
||||
## 6. Architecture
|
||||
|
||||
```
|
||||
[CRON - daily at 06:00]
|
||||
|
|
||||
v
|
||||
[adsPRO Server (PHP)]
|
||||
|
|
||||
v
|
||||
[Google Ads REST API v18]
|
||||
- SearchStream (read campaign data)
|
||||
- MutateCampaigns (manage campaigns)
|
||||
- MutateCampaignBudgets (adjust budgets)
|
||||
|
|
||||
v
|
||||
[MySQL Database]
|
||||
- campaigns table (current state)
|
||||
- campaigns_history table (daily snapshots)
|
||||
|
|
||||
v
|
||||
[adsPRO Dashboard]
|
||||
- Campaign performance charts
|
||||
- ROAS tracking over time
|
||||
- Campaign management interface
|
||||
```
|
||||
|
||||
## 7. Data Handling and Privacy
|
||||
|
||||
- All data is stored on our own private server (shared hosting with SSL)
|
||||
- Data is accessible only to authenticated agency staff (login required)
|
||||
- No campaign data is shared with third parties
|
||||
- No personally identifiable information (PII) is collected from end users
|
||||
- API credentials are stored server-side and never exposed in client-facing code
|
||||
|
||||
## 8. Users
|
||||
|
||||
This is an **internal agency tool**. It is not a publicly available application. Access is restricted to authorized staff members of our agency (currently 3 users). There is no self-registration — accounts are created by the administrator.
|
||||
|
||||
## 9. Compliance
|
||||
|
||||
- The tool complies with the Google Ads API Terms of Service
|
||||
- The tool complies with the Google API Services User Data Policy
|
||||
- No data is sold or shared with third parties
|
||||
- The tool does not perform automated campaign modifications without human oversight
|
||||
90
index.php
90
index.php
@@ -30,6 +30,53 @@ $mdb = new medoo([
|
||||
'charset' => 'utf8'
|
||||
]);
|
||||
|
||||
// --- Nowy router ---
|
||||
$request_uri = $_SERVER['REQUEST_URI'];
|
||||
$uri = parse_url($request_uri, PHP_URL_PATH);
|
||||
$uri = trim($uri, '/');
|
||||
$segments = $uri ? explode('/', $uri, 3) : [];
|
||||
|
||||
// Aliasy czystych URL na moduł/akcję
|
||||
$route_aliases = [
|
||||
'login' => ['users', 'login_form'],
|
||||
'logowanie' => ['users', 'login_form'],
|
||||
'logout' => ['users', 'logout'],
|
||||
'settings' => ['users', 'settings'],
|
||||
'settings/save' => ['users', 'settings_save'],
|
||||
'settings/save_google_ads' => ['users', 'settings_save_google_ads'],
|
||||
'settings/save_openai' => ['users', 'settings_save_openai'],
|
||||
'products/ai_suggest' => ['products', 'ai_suggest'],
|
||||
'clients/save' => ['clients', 'save'],
|
||||
];
|
||||
|
||||
$path = implode('/', $segments);
|
||||
$path_first = $segments[0] ?? '';
|
||||
|
||||
if (isset($route_aliases[$path])) {
|
||||
$_GET['module'] = $route_aliases[$path][0];
|
||||
$_GET['action'] = $route_aliases[$path][1];
|
||||
} elseif (isset($route_aliases[$path_first])) {
|
||||
$_GET['module'] = $route_aliases[$path_first][0];
|
||||
$_GET['action'] = $route_aliases[$path_first][1];
|
||||
} elseif (count($segments) >= 2) {
|
||||
$_GET['module'] = $segments[0];
|
||||
$_GET['action'] = $segments[1];
|
||||
if (isset($segments[2])) {
|
||||
parse_str($segments[2], $extra);
|
||||
$_GET = array_merge($_GET, $extra);
|
||||
}
|
||||
} elseif (count($segments) === 1 && $segments[0] !== '') {
|
||||
$_GET['module'] = $segments[0];
|
||||
$_GET['action'] = 'main_view';
|
||||
} else {
|
||||
$_GET['module'] = 'campaigns';
|
||||
$_GET['action'] = 'main_view';
|
||||
}
|
||||
|
||||
// Aktualny moduł do podświetlenia w sidebar
|
||||
$current_module = $_GET['module'] ?? '';
|
||||
|
||||
// --- Autoryzacja ---
|
||||
$domain = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] );
|
||||
$cookie_name = str_replace( '.', '-', $domain );
|
||||
|
||||
@@ -46,32 +93,25 @@ if ( isset( $_COOKIE[$cookie_name] ) && !isset( $_SESSION['user'] ) )
|
||||
}
|
||||
|
||||
$user = \S::get_session('user');
|
||||
if (
|
||||
!$user
|
||||
and
|
||||
!(
|
||||
in_array( $_SERVER['REQUEST_URI'], [ '/logowanie', '/users/login/' ] )
|
||||
or
|
||||
strpos( $_SERVER['REQUEST_URI'], '/api/campaigns_data_save/' ) !== false
|
||||
or
|
||||
strpos( $_SERVER['REQUEST_URI'], '/api/phrases_data_save/' ) !== false
|
||||
or
|
||||
strpos( $_SERVER['REQUEST_URI'], '/api/products_data_save/' ) !== false
|
||||
or
|
||||
strpos( $_SERVER['REQUEST_URI'], '/cron/cron_products/' ) !== false
|
||||
or
|
||||
strpos( $_SERVER['REQUEST_URI'], '/cron/cron_products_history_30/' ) !== false
|
||||
or
|
||||
strpos( $_SERVER['REQUEST_URI'], '/cron/cron_xml/' ) !== false
|
||||
or
|
||||
strpos( $_SERVER['REQUEST_URI'], '/cron/cron_phrases/' ) !== false
|
||||
or
|
||||
strpos( $_SERVER['REQUEST_URI'], '/cron/cron_phrases_history_30/' ) !== false
|
||||
)
|
||||
)
|
||||
|
||||
// Whitelist - strony dostępne bez logowania
|
||||
$public_paths = ['login', 'logowanie', 'users/login', 'users/login_form'];
|
||||
$public_prefixes = ['api/', 'cron/'];
|
||||
|
||||
$is_public = in_array($path, $public_paths)
|
||||
|| in_array($path_first . '/' . ($segments[1] ?? ''), $public_paths);
|
||||
|
||||
foreach ($public_prefixes as $prefix) {
|
||||
if (strpos($path, $prefix) === 0) {
|
||||
$is_public = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$user && !$is_public)
|
||||
{
|
||||
header( 'Location: /logowanie' );
|
||||
header( 'Location: /login' );
|
||||
exit;
|
||||
}
|
||||
|
||||
echo \view\Site::show();
|
||||
echo \view\Site::show();
|
||||
|
||||
1
layout/style-old.css
Normal file
1
layout/style-old.css
Normal file
File diff suppressed because one or more lines are too long
1409
layout/style-old.scss
Normal file
1409
layout/style-old.scss
Normal file
File diff suppressed because it is too large
Load Diff
1619
layout/style.css
1619
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
2929
layout/style.scss
2929
layout/style.scss
File diff suppressed because it is too large
Load Diff
18
migrations/001_google_ads_settings.sql
Normal file
18
migrations/001_google_ads_settings.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- Migracja: Tabela settings + kolumna google_ads_customer_id
|
||||
-- Data: 2026-02-15
|
||||
-- Opis: Dodaje globalną tabelę ustawień key-value oraz kolumnę Google Ads Customer ID do tabeli clients
|
||||
|
||||
-- 1. Tabela settings (globalne ustawienia aplikacji)
|
||||
CREATE TABLE IF NOT EXISTS `settings` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`setting_key` VARCHAR(100) NOT NULL,
|
||||
`setting_value` TEXT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_setting_key` (`setting_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
-- 2. Kolumna google_ads_customer_id w tabeli clients
|
||||
ALTER TABLE `clients` ADD COLUMN `google_ads_customer_id` VARCHAR(20) NULL DEFAULT NULL AFTER `name`;
|
||||
|
||||
-- 3. Kolumna google_ads_start_date w tabeli clients (data od kiedy pobierać dane z Google Ads API)
|
||||
ALTER TABLE `clients` ADD COLUMN `google_ads_start_date` DATE NULL DEFAULT NULL AFTER `google_ads_customer_id`;
|
||||
1
migrations/002_products_data_url.sql
Normal file
1
migrations/002_products_data_url.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `products_data` ADD COLUMN `product_url` VARCHAR(500) NULL DEFAULT NULL;
|
||||
361
migrations/demo_data.sql
Normal file
361
migrations/demo_data.sql
Normal file
@@ -0,0 +1,361 @@
|
||||
-- ============================================================
|
||||
-- DANE DEMO dla adsPRO
|
||||
-- Klient: pomysloweprezenty.pl (client_id = 2)
|
||||
-- Data generacji: 2026-02-15
|
||||
-- ============================================================
|
||||
-- UWAGA: Uruchom ten skrypt TYLKO na bazie testowej/deweloperskiej!
|
||||
-- Zakłada że klient o id=2 istnieje z google_ads_customer_id = '941-605-1782'
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- 1. KAMPANIE
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO `campaigns` (`client_id`, `campaign_id`, `campaign_name`) VALUES
|
||||
(2, 20845671001, 'PMAX | Prezenty personalizowane'),
|
||||
(2, 20845671002, 'PMAX | Bestsellery'),
|
||||
(2, 20845671003, 'PMAX | Walentynki 2026'),
|
||||
(2, 20845671004, 'Shopping | Wszystkie produkty'),
|
||||
(2, 20845671005, 'Shopping | Prezenty do 50 zł'),
|
||||
(2, 20845671006, 'Shopping | Prezenty premium'),
|
||||
(2, 20845671007, 'PMAX | Nowości'),
|
||||
(2, 20845671008, 'Shopping | Bestsellery high ROAS');
|
||||
|
||||
-- Zapamiętaj ID kampanii (zakładam auto_increment od 1)
|
||||
SET @camp1 = (SELECT id FROM campaigns WHERE campaign_id = 20845671001 AND client_id = 2);
|
||||
SET @camp2 = (SELECT id FROM campaigns WHERE campaign_id = 20845671002 AND client_id = 2);
|
||||
SET @camp3 = (SELECT id FROM campaigns WHERE campaign_id = 20845671003 AND client_id = 2);
|
||||
SET @camp4 = (SELECT id FROM campaigns WHERE campaign_id = 20845671004 AND client_id = 2);
|
||||
SET @camp5 = (SELECT id FROM campaigns WHERE campaign_id = 20845671005 AND client_id = 2);
|
||||
SET @camp6 = (SELECT id FROM campaigns WHERE campaign_id = 20845671006 AND client_id = 2);
|
||||
SET @camp7 = (SELECT id FROM campaigns WHERE campaign_id = 20845671007 AND client_id = 2);
|
||||
SET @camp8 = (SELECT id FROM campaigns WHERE campaign_id = 20845671008 AND client_id = 2);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. HISTORIA KAMPANII (30 dni: 2026-01-16 do 2026-02-14)
|
||||
-- ============================================================
|
||||
|
||||
-- Generujemy dane dzienne za pomocą procedury
|
||||
DELIMITER //
|
||||
DROP PROCEDURE IF EXISTS generate_campaign_history//
|
||||
CREATE PROCEDURE generate_campaign_history()
|
||||
BEGIN
|
||||
DECLARE i INT DEFAULT 0;
|
||||
DECLARE cur_date DATE;
|
||||
DECLARE day_factor FLOAT;
|
||||
|
||||
WHILE i < 30 DO
|
||||
SET cur_date = DATE_SUB('2026-02-14', INTERVAL i DAY);
|
||||
-- Weekendy mają wyższe wydatki (factor 1.2-1.4), dni powszednie normalne
|
||||
SET day_factor = CASE DAYOFWEEK(cur_date)
|
||||
WHEN 1 THEN 1.3 -- niedziela
|
||||
WHEN 7 THEN 1.4 -- sobota
|
||||
WHEN 6 THEN 1.1 -- piątek
|
||||
ELSE 1.0
|
||||
END;
|
||||
|
||||
-- Walentynki boost (7-14 lutego)
|
||||
IF cur_date BETWEEN '2026-02-07' AND '2026-02-14' THEN
|
||||
SET day_factor = day_factor * 1.5;
|
||||
END IF;
|
||||
|
||||
-- PMAX | Prezenty personalizowane (wysoki ROAS, duży budżet)
|
||||
INSERT INTO campaigns_history (campaign_id, date_add, roas_30_days, roas_all_time, budget, money_spent, conversion_value, bidding_strategy) VALUES
|
||||
(@camp1, cur_date,
|
||||
ROUND(450 + RAND() * 150, 2),
|
||||
ROUND(520 + RAND() * 80, 2),
|
||||
150.00,
|
||||
ROUND((130 + RAND() * 40) * day_factor, 2),
|
||||
ROUND((600 + RAND() * 300) * day_factor, 2),
|
||||
'Docelowy ROAS | docelowy ROAS: 400%');
|
||||
|
||||
-- PMAX | Bestsellery (bardzo wysoki ROAS)
|
||||
INSERT INTO campaigns_history (campaign_id, date_add, roas_30_days, roas_all_time, budget, money_spent, conversion_value, bidding_strategy) VALUES
|
||||
(@camp2, cur_date,
|
||||
ROUND(650 + RAND() * 200, 2),
|
||||
ROUND(700 + RAND() * 100, 2),
|
||||
200.00,
|
||||
ROUND((170 + RAND() * 60) * day_factor, 2),
|
||||
ROUND((1200 + RAND() * 500) * day_factor, 2),
|
||||
'Docelowy ROAS | docelowy ROAS: 600%');
|
||||
|
||||
-- PMAX | Walentynki 2026 (sezonowa, wysoki wydatek w lutym)
|
||||
INSERT INTO campaigns_history (campaign_id, date_add, roas_30_days, roas_all_time, budget, money_spent, conversion_value, bidding_strategy) VALUES
|
||||
(@camp3, cur_date,
|
||||
ROUND(350 + RAND() * 200 + IF(cur_date >= '2026-02-01', 100, 0), 2),
|
||||
ROUND(380 + RAND() * 120, 2),
|
||||
ROUND(IF(cur_date >= '2026-02-01', 250, 80), 2),
|
||||
ROUND((IF(cur_date >= '2026-02-01', 200, 60) + RAND() * 50) * day_factor, 2),
|
||||
ROUND((IF(cur_date >= '2026-02-01', 800, 200) + RAND() * 400) * day_factor, 2),
|
||||
'Maksymalizacja wartosci konwersji');
|
||||
|
||||
-- Shopping | Wszystkie produkty (umiarkowany ROAS, duży budżet)
|
||||
INSERT INTO campaigns_history (campaign_id, date_add, roas_30_days, roas_all_time, budget, money_spent, conversion_value, bidding_strategy) VALUES
|
||||
(@camp4, cur_date,
|
||||
ROUND(300 + RAND() * 100, 2),
|
||||
ROUND(350 + RAND() * 60, 2),
|
||||
100.00,
|
||||
ROUND((85 + RAND() * 30) * day_factor, 2),
|
||||
ROUND((280 + RAND() * 120) * day_factor, 2),
|
||||
'Docelowy ROAS | docelowy ROAS: 300%');
|
||||
|
||||
-- Shopping | Prezenty do 50 zł (niski CPC, dobry ROAS)
|
||||
INSERT INTO campaigns_history (campaign_id, date_add, roas_30_days, roas_all_time, budget, money_spent, conversion_value, bidding_strategy) VALUES
|
||||
(@camp5, cur_date,
|
||||
ROUND(380 + RAND() * 120, 2),
|
||||
ROUND(400 + RAND() * 80, 2),
|
||||
50.00,
|
||||
ROUND((40 + RAND() * 15) * day_factor, 2),
|
||||
ROUND((170 + RAND() * 80) * day_factor, 2),
|
||||
'Docelowy ROAS | docelowy ROAS: 350%');
|
||||
|
||||
-- Shopping | Prezenty premium (niski ROAS, wysoki AOV)
|
||||
INSERT INTO campaigns_history (campaign_id, date_add, roas_30_days, roas_all_time, budget, money_spent, conversion_value, bidding_strategy) VALUES
|
||||
(@camp6, cur_date,
|
||||
ROUND(200 + RAND() * 100, 2),
|
||||
ROUND(250 + RAND() * 80, 2),
|
||||
80.00,
|
||||
ROUND((65 + RAND() * 25) * day_factor, 2),
|
||||
ROUND((150 + RAND() * 100) * day_factor, 2),
|
||||
'Maksymalizacja wartosci konwersji');
|
||||
|
||||
-- PMAX | Nowości (nowa kampania, niski ROAS na start)
|
||||
INSERT INTO campaigns_history (campaign_id, date_add, roas_30_days, roas_all_time, budget, money_spent, conversion_value, bidding_strategy) VALUES
|
||||
(@camp7, cur_date,
|
||||
ROUND(180 + RAND() * 100 + i * 3, 2),
|
||||
ROUND(200 + RAND() * 80, 2),
|
||||
60.00,
|
||||
ROUND((50 + RAND() * 20) * day_factor, 2),
|
||||
ROUND((100 + RAND() * 80 + i * 5) * day_factor, 2),
|
||||
'Maksymalizacja liczby konwersji');
|
||||
|
||||
-- Shopping | Bestsellery high ROAS (najlepsza kampania)
|
||||
INSERT INTO campaigns_history (campaign_id, date_add, roas_30_days, roas_all_time, budget, money_spent, conversion_value, bidding_strategy) VALUES
|
||||
(@camp8, cur_date,
|
||||
ROUND(800 + RAND() * 300, 2),
|
||||
ROUND(850 + RAND() * 150, 2),
|
||||
120.00,
|
||||
ROUND((100 + RAND() * 35) * day_factor, 2),
|
||||
ROUND((900 + RAND() * 400) * day_factor, 2),
|
||||
'Docelowy ROAS | docelowy ROAS: 700%');
|
||||
|
||||
SET i = i + 1;
|
||||
END WHILE;
|
||||
END//
|
||||
DELIMITER ;
|
||||
|
||||
CALL generate_campaign_history();
|
||||
DROP PROCEDURE IF EXISTS generate_campaign_history;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 3. PRODUKTY (25 produktów - realistyczne nazwy sklepu z prezentami)
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO `products` (`client_id`, `offer_id`, `name`) VALUES
|
||||
(2, 'shopify_PL_8901001', 'Kubek personalizowany ze zdjęciem - Biały 330ml'),
|
||||
(2, 'shopify_PL_8901002', 'Poduszka z własnym nadrukiem 40x40cm'),
|
||||
(2, 'shopify_PL_8901003', 'Brelok LED z grawerem - Serce'),
|
||||
(2, 'shopify_PL_8901004', 'Ramka na zdjęcie z dedykacją - Drewniana A4'),
|
||||
(2, 'shopify_PL_8901005', 'Puzzle ze zdjęciem 500 elementów'),
|
||||
(2, 'shopify_PL_8901006', 'Koszulka z nadrukiem - Dla Najlepszego Taty'),
|
||||
(2, 'shopify_PL_8901007', 'Skarpetki personalizowane - Zestaw 3 par'),
|
||||
(2, 'shopify_PL_8901008', 'Kubek magiczny zmieniający kolor ze zdjęciem'),
|
||||
(2, 'shopify_PL_8901009', 'Plakat personalizowany - Mapa Gwiazd A3'),
|
||||
(2, 'shopify_PL_8901010', 'Biżuteria - Naszyjnik z grawerem Serce'),
|
||||
(2, 'shopify_PL_8901011', 'Etui na telefon z własnym zdjęciem'),
|
||||
(2, 'shopify_PL_8901012', 'Torba bawełniana z nadrukiem'),
|
||||
(2, 'shopify_PL_8901013', 'Kalendarz ze zdjęciami 2026 - Ścienny A3'),
|
||||
(2, 'shopify_PL_8901014', 'Podkładka pod mysz z własnym zdjęciem'),
|
||||
(2, 'shopify_PL_8901015', 'Zestaw upominkowy - Kubek + Podkładka + Brelok'),
|
||||
(2, 'shopify_PL_8901016', 'Obraz na płótnie Canvas 60x40cm ze zdjęciem'),
|
||||
(2, 'shopify_PL_8901017', 'Otwieracz do butelek z grawerem'),
|
||||
(2, 'shopify_PL_8901018', 'Fotokolaż na płótnie 50x70cm'),
|
||||
(2, 'shopify_PL_8901019', 'Koc z nadrukiem polarowy 150x200cm'),
|
||||
(2, 'shopify_PL_8901020', 'Piersiówka z grawerem 200ml'),
|
||||
(2, 'shopify_PL_8901021', 'Zegar ścienny ze zdjęciem - Okrągły 30cm'),
|
||||
(2, 'shopify_PL_8901022', 'Ręcznik z haftem imienia 70x140cm'),
|
||||
(2, 'shopify_PL_8901023', 'Walentynkowy zestaw - Kubek + Czekolada + Kartka'),
|
||||
(2, 'shopify_PL_8901024', 'Magnes na lodówkę z własnym zdjęciem'),
|
||||
(2, 'shopify_PL_8901025', 'Szkatułka drewniana z grawerem');
|
||||
|
||||
-- ============================================================
|
||||
-- 4. HISTORIA PRODUKTÓW (30 dni dziennych danych)
|
||||
-- ============================================================
|
||||
|
||||
DELIMITER //
|
||||
DROP PROCEDURE IF EXISTS generate_product_history//
|
||||
CREATE PROCEDURE generate_product_history()
|
||||
BEGIN
|
||||
DECLARE i INT DEFAULT 0;
|
||||
DECLARE j INT DEFAULT 1;
|
||||
DECLARE cur_date DATE;
|
||||
DECLARE prod_id INT;
|
||||
DECLARE base_imp INT;
|
||||
DECLARE base_clicks INT;
|
||||
DECLARE base_cost DECIMAL(10,2);
|
||||
DECLARE base_conv INT;
|
||||
DECLARE base_conv_val DECIMAL(10,2);
|
||||
DECLARE day_imp INT;
|
||||
DECLARE day_clicks INT;
|
||||
DECLARE day_cost DECIMAL(10,2);
|
||||
DECLARE day_conv INT;
|
||||
DECLARE day_conv_val DECIMAL(10,2);
|
||||
DECLARE day_ctr DECIMAL(6,4);
|
||||
DECLARE day_factor FLOAT;
|
||||
DECLARE product_count INT;
|
||||
|
||||
SET product_count = 25;
|
||||
|
||||
-- Dla każdego produktu
|
||||
SET j = 1;
|
||||
WHILE j <= product_count DO
|
||||
-- Pobierz ID produktu
|
||||
SET prod_id = (SELECT id FROM products WHERE client_id = 2 AND offer_id = CONCAT('shopify_PL_890100', j) LIMIT 1);
|
||||
IF prod_id IS NULL AND j >= 10 THEN
|
||||
SET prod_id = (SELECT id FROM products WHERE client_id = 2 AND offer_id = CONCAT('shopify_PL_89010', j) LIMIT 1);
|
||||
END IF;
|
||||
|
||||
-- Bazowe metryki różne per produkt (symulacja różnej popularności)
|
||||
CASE j
|
||||
WHEN 1 THEN SET base_imp = 850, base_clicks = 45, base_cost = 12.50, base_conv = 3, base_conv_val = 89.70; -- Kubek - bestseller
|
||||
WHEN 2 THEN SET base_imp = 620, base_clicks = 32, base_cost = 9.80, base_conv = 2, base_conv_val = 79.80; -- Poduszka
|
||||
WHEN 3 THEN SET base_imp = 1200, base_clicks = 68, base_cost = 15.20, base_conv = 5, base_conv_val = 74.75; -- Brelok LED - top impressions
|
||||
WHEN 4 THEN SET base_imp = 380, base_clicks = 18, base_cost = 7.20, base_conv = 1, base_conv_val = 59.90; -- Ramka
|
||||
WHEN 5 THEN SET base_imp = 520, base_clicks = 28, base_cost = 11.00, base_conv = 2, base_conv_val = 99.80; -- Puzzle
|
||||
WHEN 6 THEN SET base_imp = 780, base_clicks = 42, base_cost = 13.50, base_conv = 3, base_conv_val = 89.70; -- Koszulka
|
||||
WHEN 7 THEN SET base_imp = 450, base_clicks = 22, base_cost = 6.80, base_conv = 2, base_conv_val = 49.90; -- Skarpetki
|
||||
WHEN 8 THEN SET base_imp = 680, base_clicks = 38, base_cost = 10.50, base_conv = 2, base_conv_val = 69.90; -- Kubek magiczny
|
||||
WHEN 9 THEN SET base_imp = 920, base_clicks = 55, base_cost = 18.00, base_conv = 4, base_conv_val = 159.60; -- Plakat mapa gwiazd - wysoki AOV
|
||||
WHEN 10 THEN SET base_imp = 1100, base_clicks = 72, base_cost = 22.00, base_conv = 5, base_conv_val = 349.50; -- Naszyjnik - top conversion value
|
||||
WHEN 11 THEN SET base_imp = 550, base_clicks = 30, base_cost = 8.50, base_conv = 1, base_conv_val = 39.90; -- Etui
|
||||
WHEN 12 THEN SET base_imp = 320, base_clicks = 14, base_cost = 4.20, base_conv = 1, base_conv_val = 29.90; -- Torba
|
||||
WHEN 13 THEN SET base_imp = 280, base_clicks = 12, base_cost = 5.00, base_conv = 0, base_conv_val = 0.00; -- Kalendarz - słaby (po sezonie)
|
||||
WHEN 14 THEN SET base_imp = 180, base_clicks = 8, base_cost = 2.50, base_conv = 0, base_conv_val = 0.00; -- Podkładka - zombie
|
||||
WHEN 15 THEN SET base_imp = 740, base_clicks = 40, base_cost = 14.00, base_conv = 3, base_conv_val = 179.70; -- Zestaw upominkowy
|
||||
WHEN 16 THEN SET base_imp = 480, base_clicks = 25, base_cost = 16.00, base_conv = 1, base_conv_val = 149.00; -- Canvas
|
||||
WHEN 17 THEN SET base_imp = 350, base_clicks = 18, base_cost = 5.50, base_conv = 1, base_conv_val = 34.90; -- Otwieracz
|
||||
WHEN 18 THEN SET base_imp = 410, base_clicks = 22, base_cost = 14.00, base_conv = 1, base_conv_val = 189.00; -- Fotokolaż
|
||||
WHEN 19 THEN SET base_imp = 290, base_clicks = 15, base_cost = 9.00, base_conv = 1, base_conv_val = 119.00; -- Koc
|
||||
WHEN 20 THEN SET base_imp = 420, base_clicks = 24, base_cost = 7.00, base_conv = 1, base_conv_val = 69.90; -- Piersiówka
|
||||
WHEN 21 THEN SET base_imp = 260, base_clicks = 11, base_cost = 4.50, base_conv = 0, base_conv_val = 0.00; -- Zegar - słaby
|
||||
WHEN 22 THEN SET base_imp = 340, base_clicks = 16, base_cost = 6.00, base_conv = 1, base_conv_val = 59.90; -- Ręcznik
|
||||
WHEN 23 THEN SET base_imp = 1500, base_clicks = 95, base_cost = 28.00, base_conv = 8, base_conv_val = 319.20; -- Walentynkowy zestaw - HIT
|
||||
WHEN 24 THEN SET base_imp = 150, base_clicks = 6, base_cost = 1.80, base_conv = 0, base_conv_val = 0.00; -- Magnes - zombie
|
||||
WHEN 25 THEN SET base_imp = 380, base_clicks = 20, base_cost = 8.00, base_conv = 1, base_conv_val = 89.90; -- Szkatułka
|
||||
ELSE SET base_imp = 300, base_clicks = 15, base_cost = 5.00, base_conv = 1, base_conv_val = 50.00;
|
||||
END CASE;
|
||||
|
||||
-- 30 dni historii
|
||||
SET i = 0;
|
||||
WHILE i < 30 DO
|
||||
SET cur_date = DATE_SUB('2026-02-14', INTERVAL i DAY);
|
||||
|
||||
SET day_factor = CASE DAYOFWEEK(cur_date)
|
||||
WHEN 1 THEN 1.25
|
||||
WHEN 7 THEN 1.35
|
||||
WHEN 6 THEN 1.1
|
||||
ELSE 1.0
|
||||
END;
|
||||
|
||||
-- Walentynki boost (szczególnie produkty walentynkowe: j=23, j=10, j=9)
|
||||
IF cur_date BETWEEN '2026-02-07' AND '2026-02-14' THEN
|
||||
IF j IN (23, 10, 9, 3, 1) THEN
|
||||
SET day_factor = day_factor * 2.0;
|
||||
ELSE
|
||||
SET day_factor = day_factor * 1.3;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
SET day_imp = ROUND(base_imp * day_factor * (0.8 + RAND() * 0.4));
|
||||
SET day_clicks = ROUND(base_clicks * day_factor * (0.7 + RAND() * 0.6));
|
||||
SET day_cost = ROUND(base_cost * day_factor * (0.8 + RAND() * 0.4), 2);
|
||||
-- Konwersje: losowe z prawdopodobieństwem bazowym
|
||||
SET day_conv = FLOOR(base_conv * day_factor * (0.3 + RAND() * 1.4));
|
||||
SET day_conv_val = ROUND(IF(day_conv > 0, day_conv * (base_conv_val / GREATEST(base_conv, 1)) * (0.9 + RAND() * 0.2), 0), 2);
|
||||
SET day_ctr = IF(day_imp > 0, ROUND(day_clicks / day_imp, 4) * 100, 0);
|
||||
|
||||
IF prod_id IS NOT NULL THEN
|
||||
INSERT INTO products_history (product_id, date_add, impressions, clicks, ctr, cost, conversions, conversions_value, updated) VALUES
|
||||
(prod_id, cur_date, day_imp, day_clicks, day_ctr, day_cost, day_conv, day_conv_val, 0);
|
||||
END IF;
|
||||
|
||||
SET i = i + 1;
|
||||
END WHILE;
|
||||
|
||||
SET j = j + 1;
|
||||
END WHILE;
|
||||
END//
|
||||
DELIMITER ;
|
||||
|
||||
CALL generate_product_history();
|
||||
DROP PROCEDURE IF EXISTS generate_product_history;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 5. PRODUCTS_TEMP (zagregowane dane - jak po cron_products)
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO products_temp (product_id, name, impressions, impressions_30, clicks, clicks_30, ctr, cost, conversions, conversions_value, cpc, roas)
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
COALESCE(SUM(ph.impressions), 0),
|
||||
COALESCE(SUM(ph.impressions), 0),
|
||||
COALESCE(SUM(ph.clicks), 0),
|
||||
COALESCE(SUM(ph.clicks), 0),
|
||||
CASE WHEN SUM(ph.impressions) > 0 THEN ROUND(SUM(ph.clicks) / SUM(ph.impressions) * 100, 2) ELSE 0 END,
|
||||
COALESCE(SUM(ph.cost), 0),
|
||||
COALESCE(SUM(ph.conversions), 0),
|
||||
COALESCE(SUM(ph.conversions_value), 0),
|
||||
CASE WHEN SUM(ph.clicks) > 0 THEN ROUND(SUM(ph.cost) / SUM(ph.clicks), 6) ELSE 0 END,
|
||||
CASE WHEN SUM(ph.conversions) > 0 AND SUM(ph.cost) > 0 THEN ROUND(SUM(ph.conversions_value) / SUM(ph.cost) * 100, 2) ELSE 0 END
|
||||
FROM products p
|
||||
LEFT JOIN products_history ph ON p.id = ph.product_id
|
||||
WHERE p.client_id = 2
|
||||
GROUP BY p.id, p.name;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 6. PRODUCTS_DATA (custom labels)
|
||||
-- ============================================================
|
||||
|
||||
-- Bestsellery (wysoki ROAS + dużo konwersji)
|
||||
INSERT INTO products_data (product_id, custom_label_4) VALUES
|
||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901001' AND client_id = 2), 'bestseller'),
|
||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901003' AND client_id = 2), 'bestseller'),
|
||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901010' AND client_id = 2), 'bestseller'),
|
||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901023' AND client_id = 2), 'bestseller');
|
||||
|
||||
-- Produkty PLA (w kampaniach Shopping)
|
||||
INSERT INTO products_data (product_id, custom_label_4) VALUES
|
||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901005' AND client_id = 2), 'pla'),
|
||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901006' AND client_id = 2), 'pla'),
|
||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901015' AND client_id = 2), 'pla');
|
||||
|
||||
-- Zombie (bardzo niskie wyświetlenia)
|
||||
INSERT INTO products_data (product_id, custom_label_4) VALUES
|
||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901014' AND client_id = 2), 'zombie'),
|
||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901024' AND client_id = 2), 'zombie');
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 7. MIN ROAS per klient (bestseller threshold)
|
||||
-- ============================================================
|
||||
|
||||
-- Ustaw min ROAS dla wybranych produktów
|
||||
UPDATE products SET min_roas = 500 WHERE offer_id = 'shopify_PL_8901001' AND client_id = 2;
|
||||
UPDATE products SET min_roas = 400 WHERE offer_id = 'shopify_PL_8901003' AND client_id = 2;
|
||||
UPDATE products SET min_roas = 600 WHERE offer_id = 'shopify_PL_8901010' AND client_id = 2;
|
||||
UPDATE products SET min_roas = 350 WHERE offer_id = 'shopify_PL_8901023' AND client_id = 2;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- GOTOWE!
|
||||
-- Po wykonaniu tego skryptu:
|
||||
-- 1. Wejdź na /campaigns → wybierz klienta pomysloweprezenty.pl
|
||||
-- → zobaczysz 8 kampanii z 30-dniową historią
|
||||
-- 2. Wejdź na /products → wybierz klienta
|
||||
-- → zobaczysz 25 produktów z metrykami
|
||||
-- 3. Kliknij dowolny produkt → zobaczysz historię i wykres
|
||||
-- ============================================================
|
||||
@@ -1,361 +1,336 @@
|
||||
<div class="admin-form theme-primary">
|
||||
<div class="panel heading-border panel-primary">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="field select">
|
||||
<select id="client_id" name="client_id">
|
||||
<option value="">--- wybierz klienta ---</option>
|
||||
<? foreach ( $this -> clients as $client ):?>
|
||||
<option value="<?= $client['id'];?>"><?= $client['name'];?></option>
|
||||
<? endforeach;?>
|
||||
</select>
|
||||
<i class="arrow double"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<label class="field select">
|
||||
<select id="campaign_id" name="campaign_id">
|
||||
<option value="">--- wybierz kampanię ---</option>
|
||||
</select>
|
||||
<i class="arrow double"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="button" id="delete_campaign" class="btn btn-danger btn-block" title="Usuń kampanię">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="campaigns-page">
|
||||
<div class="campaigns-header">
|
||||
<h2><i class="fa-solid fa-chart-line"></i> Kampanie</h2>
|
||||
</div>
|
||||
|
||||
<!-- Filtry -->
|
||||
<div class="campaigns-filters">
|
||||
<div class="filter-group">
|
||||
<label for="client_id"><i class="fa-solid fa-building"></i> Klient</label>
|
||||
<select id="client_id" name="client_id" class="form-control">
|
||||
<option value="">— wybierz klienta —</option>
|
||||
<?php foreach ( $this -> clients as $client ): ?>
|
||||
<option value="<?= $client['id']; ?>"><?= htmlspecialchars( $client['name'] ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label for="campaign_id"><i class="fa-solid fa-bullhorn"></i> Kampania</label>
|
||||
<div class="filter-with-action">
|
||||
<select id="campaign_id" name="campaign_id" class="form-control">
|
||||
<option value="">— wybierz kampanię —</option>
|
||||
</select>
|
||||
<button type="button" id="delete_campaign" class="btn-icon btn-icon-delete" title="Usuń kampanię">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-form theme-primary">
|
||||
<div class="panel heading-border panel-primary">
|
||||
<div class="panel-body">
|
||||
<figure class="highcharts-figure">
|
||||
<div id="container"></div>
|
||||
</figure>
|
||||
</div>
|
||||
<!-- Wykres -->
|
||||
<div class="campaigns-chart-wrap">
|
||||
<div id="container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Tabela historii -->
|
||||
<div class="campaigns-table-wrap">
|
||||
<table class="table" id="products">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Data</th>
|
||||
<th>ROAS (30 dni)</th>
|
||||
<th>ROAS (all time)</th>
|
||||
<th>Wartość konwersji (30 dni)</th>
|
||||
<th>Wydatki (30 dni)</th>
|
||||
<th>Komentarz</th>
|
||||
<th>Strategia ustalania stawek</th>
|
||||
<th>Budżet</th>
|
||||
<th style="width: 60px; text-align: center;">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</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">Data</th>
|
||||
<th scope="col">ROAS (30 dni)</th>
|
||||
<th scope="col">ROAS (all time)</th>
|
||||
<th scope="col">Wartość konwersji (30 dni)</th>
|
||||
<th scope="col">Wydatki (30 dni)</th>
|
||||
<th scope="col">Komentarz</th>
|
||||
<th scope="col">Strategia ustalania stawek</th>
|
||||
<th scope="col">Budżet</th>
|
||||
<th scope="col">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
var client_id = '';
|
||||
var client_id = '';
|
||||
|
||||
function reloadChart()
|
||||
function reloadChart()
|
||||
{
|
||||
var campaign_id = $( '#campaign_id' ).val();
|
||||
if ( !campaign_id ) return;
|
||||
|
||||
$.ajax({
|
||||
url: '/campaigns/get_campaign_history_data_table_chart/',
|
||||
method: 'POST',
|
||||
data: { campaign_id: campaign_id },
|
||||
success: function( response )
|
||||
{
|
||||
const parsedData = JSON.parse( response );
|
||||
let plotLines = [];
|
||||
|
||||
parsedData.comments.forEach( function( comment ) {
|
||||
plotLines.push({
|
||||
color: '#333333',
|
||||
width: 1,
|
||||
value: parsedData.dates.indexOf( comment.date_add.split(' ')[0] ),
|
||||
dashStyle: 'Solid',
|
||||
label: {
|
||||
text: comment.comment,
|
||||
align: 'left',
|
||||
style: { color: '#333333', fontSize: '13px' }
|
||||
},
|
||||
zIndex: 5
|
||||
});
|
||||
});
|
||||
|
||||
Highcharts.chart( 'container', {
|
||||
chart: {
|
||||
style: { fontFamily: '"Open Sans", sans-serif' },
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
title: { text: '' },
|
||||
subtitle: { text: '' },
|
||||
yAxis: {
|
||||
title: { text: '' },
|
||||
gridLineColor: '#E2E8F0'
|
||||
},
|
||||
xAxis: {
|
||||
categories: parsedData.dates,
|
||||
labels: {
|
||||
style: { fontSize: '12px', color: '#8899A6' },
|
||||
formatter: function() {
|
||||
var date = new Date( Date.parse( this.value ) );
|
||||
var day = date.getDate();
|
||||
var month = date.getMonth() + 1;
|
||||
var year = date.getFullYear();
|
||||
if ( day === 1 || this.isLast ) {
|
||||
return year + '-' + ( month < 10 ? '0' + month : month ) + '-' + ( day < 10 ? '0' + day : day );
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
plotLines: plotLines
|
||||
},
|
||||
legend: {
|
||||
layout: 'horizontal',
|
||||
align: 'center',
|
||||
verticalAlign: 'bottom',
|
||||
itemStyle: { fontSize: '13px', color: '#4E5E6A' }
|
||||
},
|
||||
plotOptions: {
|
||||
series: {
|
||||
label: { connectorAllowed: false },
|
||||
pointStart: 0
|
||||
}
|
||||
},
|
||||
colors: ['#6690F4', '#57B951', '#FF8C00', '#CC0000', '#8B5CF6'],
|
||||
series: parsedData.chart_data,
|
||||
tooltip: { style: { fontSize: '13px' } },
|
||||
credits: { enabled: false }
|
||||
});
|
||||
},
|
||||
error: function( jqXHR, textStatus, errorThrown ) {
|
||||
console.error( 'Error AJAX:', textStatus, errorThrown );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$( function()
|
||||
{
|
||||
// Załaduj kampanie po wyborze klienta
|
||||
$( 'body' ).on( 'change', '#client_id', function()
|
||||
{
|
||||
var campaign_id = $( '#campaign_id' ).val();
|
||||
if ( !campaign_id ) return;
|
||||
client_id = $( this ).val();
|
||||
var campaigns_select = $( '#campaign_id' );
|
||||
|
||||
$.ajax({
|
||||
url: '/campaigns/get_campaign_history_data_table_chart/',
|
||||
method: 'POST',
|
||||
data: {
|
||||
campaign_id: campaign_id
|
||||
},
|
||||
success: function(response) {
|
||||
const parsedData = JSON.parse(response);
|
||||
url: '/campaigns/get_campaigns_list/client_id=' + client_id,
|
||||
type: 'GET',
|
||||
success: function( response )
|
||||
{
|
||||
var data = JSON.parse( response );
|
||||
campaigns_select.empty();
|
||||
campaigns_select.append( '<option value="">— wybierz kampanię —</option>' );
|
||||
|
||||
let plotLines = [];
|
||||
var campaigns = Object.entries( data.campaigns );
|
||||
|
||||
parsedData.comments.forEach(function(comment) {
|
||||
plotLines.push({
|
||||
color: '#333333',
|
||||
width: 1,
|
||||
value: parsedData.dates.indexOf(comment.date_add.split(' ')[0]),
|
||||
dashStyle: 'Solid',
|
||||
label: {
|
||||
text: comment.comment,
|
||||
align: 'left',
|
||||
style: {
|
||||
color: '#333333',
|
||||
fontSize: '14px'
|
||||
}
|
||||
},
|
||||
zIndex: 5
|
||||
});
|
||||
campaigns.sort( function( a, b ) {
|
||||
if ( a[1] === "--- konto ---" ) return -1;
|
||||
if ( b[1] === "--- konto ---" ) return 1;
|
||||
return a[1] > b[1] ? 1 : ( a[1] < b[1] ? -1 : 0 );
|
||||
});
|
||||
|
||||
Highcharts.chart('container', {
|
||||
title: {
|
||||
text: ``,
|
||||
},
|
||||
subtitle: {
|
||||
text: ``,
|
||||
},
|
||||
yAxis: {
|
||||
title: {
|
||||
text: ''
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
categories: parsedData.dates,
|
||||
labels: {
|
||||
style: {
|
||||
fontSize: '14px'
|
||||
},
|
||||
formatter: function() {
|
||||
var date = new Date(Date.parse(this.value));
|
||||
var day = date.getDate();
|
||||
var month = date.getMonth() + 1;
|
||||
var year = date.getFullYear();
|
||||
|
||||
if (day === 1 || this.isLast) {
|
||||
return `${year}-${month < 10 ? '0' + month : month}-${day < 10 ? '0' + day : day}`;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
plotLines: plotLines
|
||||
},
|
||||
legend: {
|
||||
layout: 'vertical',
|
||||
align: 'right',
|
||||
verticalAlign: 'middle',
|
||||
itemStyle: {
|
||||
fontSize: '14px'
|
||||
}
|
||||
},
|
||||
plotOptions: {
|
||||
series: {
|
||||
label: {
|
||||
connectorAllowed: false
|
||||
},
|
||||
pointStart: 0,
|
||||
},
|
||||
},
|
||||
series: parsedData.chart_data,
|
||||
tooltip: {
|
||||
style: {
|
||||
fontSize: '14px'
|
||||
}
|
||||
}
|
||||
campaigns.forEach( function( [key, value] ) {
|
||||
campaigns_select.append( '<option value="' + value.id + '">' + value.campaign_name + '</option>' );
|
||||
});
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
console.error('Error AJAX:', textStatus, errorThrown);
|
||||
|
||||
<?php if ( $campaign_id ): ?>
|
||||
campaigns_select.val( '<?= $campaign_id; ?>' ).trigger( 'change' );
|
||||
<?php endif; ?>
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$( function()
|
||||
{
|
||||
// load campaigns from server when client is selected
|
||||
$( 'body' ).on( 'change', '#client_id', function()
|
||||
{
|
||||
client_id = $( this ).val();
|
||||
var campaigns_select = $( '#campaign_id' );
|
||||
|
||||
$.ajax(
|
||||
{
|
||||
url: '/campaigns/get_campaigns_list/client_id=' + client_id,
|
||||
type: 'GET',
|
||||
success: function( response )
|
||||
{
|
||||
var data = JSON.parse(response);
|
||||
|
||||
campaigns_select.empty();
|
||||
campaigns_select.append('<option value="">- wybierz kampanię -</option>');
|
||||
|
||||
var campaigns = Object.entries(data.campaigns);
|
||||
|
||||
// Sortowanie kampanii: "--- konto ---" zawsze na początku, reszta alfabetycznie
|
||||
campaigns.sort(function(a, b) {
|
||||
if (a[1] === "--- konto ---") {
|
||||
return -1; // "a" idzie na początek
|
||||
} else if (b[1] === "--- konto ---") {
|
||||
return 1; // "b" idzie na początek
|
||||
} else {
|
||||
// Porównanie ciągów znaków bez użycia localeCompare
|
||||
return a[1] > b[1] ? 1 : (a[1] < b[1] ? -1 : 0);
|
||||
}
|
||||
});
|
||||
|
||||
// Dodawanie opcji do selecta
|
||||
campaigns.forEach(function([key, value]) {
|
||||
campaigns_select.append('<option value="' + value.id + '">' + value.campaign_name + '</option>');
|
||||
});
|
||||
|
||||
<? if ( $campaign_id ): ?>
|
||||
campaigns_select.val('<?= $campaign_id; ?>').trigger('change');
|
||||
<? endif; ?>
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$( '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ę <strong>' + campaign_name + '</strong>?<br><br>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'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$( '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 <strong>' + date + '</strong>?',
|
||||
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'
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$( '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,
|
||||
columns: [
|
||||
{ width: '100px', name: 'date', orderable: false },
|
||||
{ width: '200px', name: 'roas30', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: '200px', name: 'roas_all_time', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: '250px', name: 'conversion_value', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: '200px', name: 'spend30', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: 'auto', name: 'bidding_strategy', orderable: false },
|
||||
{ width: 'auto', name: 'comment', orderable: false },
|
||||
{ width: '150px', name: 'budget', orderable: false, className: "dt-type-numeric" },
|
||||
{ width: '80px', name: 'actions', orderable: false, className: "dt-center" }
|
||||
],
|
||||
});
|
||||
|
||||
reloadChart();
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
// 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ę <strong>' + campaign_name + '</strong>?<br><br>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 <strong>' + date + '</strong>?',
|
||||
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();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
158
templates/clients/main_view.php
Normal file
158
templates/clients/main_view.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<div class="clients-page">
|
||||
<div class="clients-header">
|
||||
<h2><i class="fa-solid fa-building"></i> Klienci</h2>
|
||||
<button type="button" class="btn btn-success" onclick="openClientForm()">
|
||||
<i class="fa-solid fa-plus mr5"></i>Dodaj klienta
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="clients-table-wrap">
|
||||
<table class="table" id="clients-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 60px;">#ID</th>
|
||||
<th>Nazwa klienta</th>
|
||||
<th>Google Ads Customer ID</th>
|
||||
<th>Dane od</th>
|
||||
<th style="width: 120px; text-align: center;">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ( $this -> clients ): ?>
|
||||
<?php foreach ( $this -> clients as $client ): ?>
|
||||
<tr data-id="<?= $client['id']; ?>">
|
||||
<td class="client-id"><?= $client['id']; ?></td>
|
||||
<td class="client-name"><?= htmlspecialchars( $client['name'] ); ?></td>
|
||||
<td>
|
||||
<?php if ( $client['google_ads_customer_id'] ): ?>
|
||||
<span class="badge-id"><?= htmlspecialchars( $client['google_ads_customer_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']; ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">— brak —</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="actions-cell">
|
||||
<button type="button" class="btn-icon btn-icon-edit" onclick="editClient(<?= $client['id']; ?>)" title="Edytuj">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
<button type="button" class="btn-icon btn-icon-delete" onclick="deleteClient(<?= $client['id']; ?>, '<?= htmlspecialchars( addslashes( $client['name'] ) ); ?>')" title="Usuń">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="5" class="empty-state">
|
||||
<i class="fa-solid fa-building"></i>
|
||||
<p>Brak klientów. Dodaj pierwszego klienta.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Dodaj / Edytuj klienta -->
|
||||
<div class="default_popup" id="client-modal">
|
||||
<div class="popup_content" style="max-width: 520px;">
|
||||
<div class="popup_header">
|
||||
<div class="title" id="client-modal-title">Dodaj klienta</div>
|
||||
<div class="close"><i class="fa-solid fa-xmark"></i></div>
|
||||
</div>
|
||||
<div class="popup_body">
|
||||
<form method="POST" id="client-form" action="/clients/save">
|
||||
<input type="hidden" name="id" id="client-id" value="" />
|
||||
<div class="settings-field">
|
||||
<label for="client-name">Nazwa klienta</label>
|
||||
<input type="text" id="client-name" name="name" class="form-control" required placeholder="np. Firma XYZ" />
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<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-gads-start">Pobieraj dane od</label>
|
||||
<input type="date" id="client-gads-start" name="google_ads_start_date" class="form-control" />
|
||||
<small class="text-muted">Data od której CRON zacznie pobierać dane z Google Ads API</small>
|
||||
</div>
|
||||
<div style="margin-top: 20px; display: flex; gap: 10px;">
|
||||
<button type="submit" class="btn btn-success"><i class="fa-solid fa-check mr5"></i>Zapisz</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="closeClientForm()">Anuluj</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function openClientForm()
|
||||
{
|
||||
$( '#client-modal-title' ).text( 'Dodaj klienta' );
|
||||
$( '#client-id' ).val( '' );
|
||||
$( '#client-name' ).val( '' );
|
||||
$( '#client-gads-id' ).val( '' );
|
||||
$( '#client-gads-start' ).val( '' );
|
||||
$( '#client-modal' ).fadeIn();
|
||||
}
|
||||
|
||||
function closeClientForm()
|
||||
{
|
||||
$( '#client-modal' ).fadeOut();
|
||||
}
|
||||
|
||||
function editClient( id )
|
||||
{
|
||||
$.getJSON( '/clients/get/id=' + id, function( data ) {
|
||||
$( '#client-modal-title' ).text( 'Edytuj klienta' );
|
||||
$( '#client-id' ).val( data.id );
|
||||
$( '#client-name' ).val( data.name );
|
||||
$( '#client-gads-id' ).val( data.google_ads_customer_id || '' );
|
||||
$( '#client-gads-start' ).val( data.google_ads_start_date || '' );
|
||||
$( '#client-modal' ).fadeIn();
|
||||
} );
|
||||
}
|
||||
|
||||
function deleteClient( id, name )
|
||||
{
|
||||
$.confirm( {
|
||||
title: 'Potwierdzenie',
|
||||
content: 'Czy na pewno chcesz usunąć klienta <strong>' + name + '</strong>?',
|
||||
type: 'red',
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: 'Usuń',
|
||||
btnClass: 'btn-red',
|
||||
action: function() {
|
||||
$.post( '/clients/delete', { id: id }, function( response ) {
|
||||
var data = JSON.parse( response );
|
||||
if ( data.success ) {
|
||||
$( 'tr[data-id="' + id + '"]' ).fadeOut( 300, function() { $( this ).remove(); } );
|
||||
}
|
||||
} );
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
text: 'Anuluj'
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
// Zamknij modal klawiszem Escape
|
||||
$( document ).on( 'keydown', function( e ) {
|
||||
if ( e.key === 'Escape' ) closeClientForm();
|
||||
} );
|
||||
|
||||
// Zamknij modal kliknięciem w tło
|
||||
$( '#client-modal' ).on( 'click', function( e ) {
|
||||
if ( e.target === this ) closeClientForm();
|
||||
} );
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,14 +4,13 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>crmPro</title>
|
||||
<meta name="keywords" content="">
|
||||
<meta name="description" content="">
|
||||
<meta name="robots" content="all">
|
||||
<title>adsPRO</title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link href="/layout/favicon.png" rel="icon" type="image/x-icon">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/2.1.7/js/dataTables.min.js"></script>
|
||||
@@ -26,11 +25,9 @@
|
||||
<script src="/libraries/select2/js/select2.full.min.js"></script>
|
||||
<script src="/libraries/functions.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/2.1.7/css/dataTables.bootstrap5.min.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<link rel="Stylesheet" type="text/css" href="/libraries/framework/skin/default_skin/css/theme.css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/datepicker/css/bootstrap-datetimepicker.css">
|
||||
<link rel="Stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/daterange/daterangepicker.css">
|
||||
<link rel="Stylesheet" type="text/css" href="/libraries/framework/admin-tools/admin-forms/css/admin-forms.css">
|
||||
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/daterange/daterangepicker.css">
|
||||
<link rel="stylesheet" type="text/css" href="/libraries/jquery-confirm/jquery-confirm.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/libraries/select2/css/select2.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="/libraries/select2/css/select2-bootstrap-5-theme.min.css">
|
||||
@@ -38,71 +35,146 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
</head>
|
||||
<body class="logged">
|
||||
<div class="top">
|
||||
<div class="logo">
|
||||
<a href="/">crm<span>Pro</span></a>
|
||||
</div>
|
||||
<div class="user-nav">
|
||||
<div class="trigger">
|
||||
<i class="fa fa-user"></i> <span><?= $this -> user[ 'email' ];?></span>
|
||||
<?php
|
||||
$module = $this -> current_module;
|
||||
?>
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-logo">
|
||||
<a href="/">
|
||||
<span class="logo-text">ads<strong>PRO</strong></span>
|
||||
</a>
|
||||
</div>
|
||||
<button class="sidebar-toggle" id="sidebarToggle">
|
||||
<i class="fa-solid fa-angles-left"></i>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/users/settings/">Ustawienia</a>
|
||||
<li class="<?= $module === 'campaigns' ? 'active' : '' ?>">
|
||||
<a href="/campaigns">
|
||||
<i class="fa-solid fa-bullhorn"></i>
|
||||
<span>Kampanie</span>
|
||||
</a>
|
||||
</li>
|
||||
<li id="divider"></li>
|
||||
<li>
|
||||
<a href="/users/logout/">Wyloguj się</a>
|
||||
<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 === 'allegro' ? 'active' : '' ?>">
|
||||
<a href="/allegro">
|
||||
<i class="fa-solid fa-file-import"></i>
|
||||
<span>Allegro import</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-divider"></li>
|
||||
<li class="<?= $module === 'users' ? 'active' : '' ?>">
|
||||
<a href="/settings">
|
||||
<i class="fa-solid fa-gear"></i>
|
||||
<span>Ustawienia</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="sidebar-user">
|
||||
<div class="user-avatar">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<span class="user-email"><?= $this -> user['email']; ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/logout" class="sidebar-logout" title="Wyloguj się">
|
||||
<i class="fa-solid fa-right-from-bracket"></i>
|
||||
<span>Wyloguj</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="main-wrapper" id="mainWrapper">
|
||||
<header class="topbar">
|
||||
<button class="topbar-toggle" id="topbarToggle">
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</button>
|
||||
<div class="topbar-breadcrumb">
|
||||
<?php
|
||||
$breadcrumbs = [
|
||||
'campaigns' => 'Kampanie',
|
||||
'products' => 'Produkty',
|
||||
'clients' => 'Klienci',
|
||||
'allegro' => 'Allegro import',
|
||||
'users' => 'Ustawienia',
|
||||
];
|
||||
echo $breadcrumbs[$module] ?? 'adsPRO';
|
||||
?>
|
||||
</div>
|
||||
</header>
|
||||
<main class="content">
|
||||
<? if ( $this -> alert ): ?>
|
||||
<div class="app-alert"><?= $this -> alert; ?></div>
|
||||
<? endif; ?>
|
||||
<?= $this -> content; ?>
|
||||
</main>
|
||||
</div>
|
||||
<div class="main-menu">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/campaigns/main_view/">Kampanie</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/products/main_view/">Produkty</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/allegro/main_view/">Allegro import</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="main">
|
||||
<? if ( $this -> alert ):?>
|
||||
<div class="alert"><?= $this -> alert;?></div>
|
||||
<? endif;?>
|
||||
<?= $this -> content;?>
|
||||
</div>
|
||||
|
||||
<!-- Popup domyslny -->
|
||||
<div class="default_popup">
|
||||
<div class="popup_content">
|
||||
<div class="popup_header">
|
||||
<div class="title"></div>
|
||||
<div class="close"><i class="fa fa-times"></i></div>
|
||||
<div class="close"><i class="fa-solid fa-xmark"></i></div>
|
||||
</div>
|
||||
<div class="popup_body"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">$
|
||||
$( function() {
|
||||
$( 'input.date' ).datepicker({
|
||||
|
||||
<script type="text/javascript">
|
||||
// Sidebar toggle
|
||||
$(function() {
|
||||
var sidebar = $('#sidebar');
|
||||
var mainWrapper = $('#mainWrapper');
|
||||
var collapsed = localStorage.getItem('sidebar_collapsed') === 'true';
|
||||
|
||||
if (collapsed) {
|
||||
sidebar.addClass('collapsed');
|
||||
mainWrapper.addClass('expanded');
|
||||
}
|
||||
|
||||
$('#sidebarToggle, #topbarToggle').on('click', function() {
|
||||
sidebar.toggleClass('collapsed');
|
||||
mainWrapper.toggleClass('expanded');
|
||||
localStorage.setItem('sidebar_collapsed', sidebar.hasClass('collapsed'));
|
||||
});
|
||||
|
||||
// Datepicker
|
||||
$('input.date').datepicker({
|
||||
language: 'pl',
|
||||
autoClose: true
|
||||
});
|
||||
|
||||
// Popup close
|
||||
$('body').on('click', '.default_popup .close', function(e) {
|
||||
e.preventDefault();
|
||||
$('.default_popup .popup_body').empty();
|
||||
$('.default_popup').hide();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'click', '.default_popup .close', function(e) {
|
||||
e.preventDefault();
|
||||
$( '.default_popup .popup_body' ).empty();
|
||||
$( '.default_popup' ).hide();
|
||||
return false;
|
||||
});
|
||||
|
||||
function show_default_popup( content ) {
|
||||
$( '.default_popup .popup_body' ).html( content );
|
||||
$( '.default_popup' ).fadeIn();
|
||||
function show_default_popup(content) {
|
||||
$('.default_popup .popup_body').html(content);
|
||||
$('.default_popup').fadeIn();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -4,18 +4,41 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>adsPRO</title>
|
||||
<meta name="keywords" content="">
|
||||
<meta name="description" content="">
|
||||
<meta name="robots" content="all">
|
||||
<title>adsPRO - Logowanie</title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/layout/style.css">
|
||||
<link href="/layout/favicon.png" rel="icon" type="image/x-icon">
|
||||
</head>
|
||||
<body class="unlogged">
|
||||
<?= $this -> content;?>
|
||||
<div class="login-container">
|
||||
<div class="login-brand">
|
||||
<div class="brand-content">
|
||||
<div class="brand-logo">ads<strong>PRO</strong></div>
|
||||
<p class="brand-tagline">System zarządzania reklamami<br>Google ADS & Facebook ADS</p>
|
||||
<div class="brand-features">
|
||||
<div class="feature">
|
||||
<i class="fa-solid fa-chart-line"></i>
|
||||
<span>Analiza ROAS i wydajności</span>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<i class="fa-solid fa-bullhorn"></i>
|
||||
<span>Zarządzanie kampaniami</span>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<i class="fa-solid fa-box-open"></i>
|
||||
<span>Optymalizacja produktów</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-form-wrapper">
|
||||
<?= $this -> content; ?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,101 +1,93 @@
|
||||
<div class="box box-login">
|
||||
<div class="title"><span>crmPRO</span> | logowanie</div>
|
||||
<div class="alert login"></div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<form method="POST" class="login-form">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-user-circle-o"></i>
|
||||
</span>
|
||||
<div class="form-line">
|
||||
<input type="text" class="form-control" name="email" id="email" placeholder="Email" autofocus="" >
|
||||
<p class="form-error hide">Uzupełnij adres email.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock"></i></span>
|
||||
<div class="form-line">
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="Hasło" >
|
||||
<p class="form-error hide">Uzupełnij hasło.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="form-line checkbox">
|
||||
<input type="checkbox" id="remeber" name="remeber">Zapamiętaj mnie
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary">Zaloguj się</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<h1>Witaj ponownie</h1>
|
||||
<p>Zaloguj się do swojego konta</p>
|
||||
</div>
|
||||
<div class="alert login"></div>
|
||||
<form method="POST" class="login-form">
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<div class="input-with-icon">
|
||||
<i class="fa-solid fa-envelope"></i>
|
||||
<input type="text" class="form-control" name="email" id="email" placeholder="Wpisz adres email" autofocus>
|
||||
</div>
|
||||
<p class="form-error hide">Uzupełnij adres email.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Hasło</label>
|
||||
<div class="input-with-icon">
|
||||
<i class="fa-solid fa-lock"></i>
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="Wpisz hasło">
|
||||
</div>
|
||||
<p class="form-error hide">Uzupełnij hasło.</p>
|
||||
</div>
|
||||
<div class="form-group checkbox-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="remember" name="remember">
|
||||
<span>Zapamiętaj mnie</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary btn-login">
|
||||
<span>Zaloguj się</span>
|
||||
<i class="fa-solid fa-arrow-right"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$( function()
|
||||
{
|
||||
$( 'body' ).on( 'keypress', '.login-form input', function(e)
|
||||
{
|
||||
if ( e.which === 13 )
|
||||
$(function() {
|
||||
$('body').on('keypress', '.login-form input', function(e) {
|
||||
if (e.which === 13)
|
||||
submit_login_form();
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'click', '.login-form .btn', function()
|
||||
{
|
||||
$('body').on('click', '.btn-login', function() {
|
||||
submit_login_form();
|
||||
});
|
||||
|
||||
function submit_login_form()
|
||||
{
|
||||
var email = $( '#email' ).val();
|
||||
if ( $.trim( email ) === '' )
|
||||
{
|
||||
$( '#email' ).parents( '.form-line' ).children( 'p' ).removeClass( 'hide' );
|
||||
$( '#email' ).focus();
|
||||
function submit_login_form() {
|
||||
var email = $('#email').val();
|
||||
if ($.trim(email) === '') {
|
||||
$('#email').parents('.form-group').find('.form-error').removeClass('hide');
|
||||
$('#email').focus();
|
||||
return false;
|
||||
} else
|
||||
$( '#email' ).parents( '.form-line' ).children( 'p' ).addClass( 'hide' );
|
||||
$('#email').parents('.form-group').find('.form-error').addClass('hide');
|
||||
|
||||
var password = $( '#password' ).val();
|
||||
if ( $.trim( password ) === '' )
|
||||
{
|
||||
$( '#password' ).parents( '.form-line' ).children( 'p' ).removeClass( 'hide' );
|
||||
$( '#password' ).focus();
|
||||
var password = $('#password').val();
|
||||
if ($.trim(password) === '') {
|
||||
$('#password').parents('.form-group').find('.form-error').removeClass('hide');
|
||||
$('#password').focus();
|
||||
return false;
|
||||
} else
|
||||
$( '#password' ).parents( '.form-line' ).children( 'p' ).addClass( 'hide' );
|
||||
$('#password').parents('.form-group').find('.form-error').addClass('hide');
|
||||
|
||||
$.ajax(
|
||||
{
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
url: '/users/login/',
|
||||
data:
|
||||
{
|
||||
data: {
|
||||
email: email,
|
||||
password: password,
|
||||
remember: $( '#remeber' ).is( ':checked' )
|
||||
remember: $('#remember').is(':checked')
|
||||
},
|
||||
beforeSend: function()
|
||||
{
|
||||
$( '.login-form .btn' ).html( '<i class="fa fa-spinner spin"></i>' ).addClass( 'disabled' );
|
||||
beforeSend: function() {
|
||||
$('.btn-login').html('<i class="fa-solid fa-spinner fa-spin"></i> Logowanie...').addClass('disabled');
|
||||
},
|
||||
success: function( response )
|
||||
{
|
||||
$( '.login-form .btn' ).html( 'Zaloguj się' ).removeClass( 'disabled' );
|
||||
success: function(response) {
|
||||
$('.btn-login').html('<span>Zaloguj się</span> <i class="fa-solid fa-arrow-right"></i>').removeClass('disabled');
|
||||
|
||||
data = jQuery.parseJSON( response );
|
||||
$( '.alert' ).html( data.msg );
|
||||
data = jQuery.parseJSON(response);
|
||||
$('.alert.login').html(data.msg);
|
||||
|
||||
if ( data.result === 'false' )
|
||||
$( '.alert' ).removeClass( 'alert-success' ).addClass( 'alert-danger' ).show();
|
||||
else
|
||||
{
|
||||
$( '.alert' ).removeClass( 'alert-danger' ).addClass( 'alert-success' ).show();
|
||||
document.location.href = '/campaigns/main_view/';
|
||||
if (data.result === 'false')
|
||||
$('.alert.login').removeClass('alert-success').addClass('alert-danger').show();
|
||||
else {
|
||||
$('.alert.login').removeClass('alert-danger').addClass('alert-success').show();
|
||||
document.location.href = '/';
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,84 +1,154 @@
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="g-container">
|
||||
<div class="block-header">
|
||||
<h2>pushover <strong>API</strong></h2>
|
||||
<small class="text-muted">uzupełnił dane potrzebne do wysyłania powiadomień PUSH</small>
|
||||
</div>
|
||||
<div id="g-form-container">
|
||||
<form method="POST" id="pushover-settings" class="g-form form-horizontal" action="/users/settings_save/">
|
||||
<?= \Html::input( [
|
||||
'label' => 'Pushover API',
|
||||
'name' => 'pushover_api',
|
||||
'value' => $this -> user['pushover_api'],
|
||||
'inline' => false
|
||||
]
|
||||
);?>
|
||||
<?= \Html::input( [
|
||||
'label' => 'Pushover User',
|
||||
'name' => 'pushover_user',
|
||||
'value' => $this -> user['pushover_user'],
|
||||
'inline' => false
|
||||
]
|
||||
);?>
|
||||
<?= \Html::button( [
|
||||
'class' => 'btn-success',
|
||||
'text' => 'Zapisz ustawienia',
|
||||
'icon' => 'fa-check',
|
||||
'js' => '$( "#pushover-settings" ).submit();'
|
||||
]
|
||||
);?>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="g-container">
|
||||
<div class="block-header">
|
||||
<h2>zmiana <strong>hasła</strong></h2>
|
||||
<small class="text-muted">zmień swoje stare hasło na nowe</small>
|
||||
</div>
|
||||
<div id="g-form-container">
|
||||
<form method="POST" id="password-settings" class="g-form form-horizontal" action="/users/password_change/">
|
||||
<?= \Html::input_icon( [
|
||||
'label' => 'Stare hasło',
|
||||
'name' => 'password_old',
|
||||
'type' => 'password',
|
||||
'inline' => false,
|
||||
'required' => true,
|
||||
'icon_content' => '<i class="fa fa-eye"></i>',
|
||||
'icon_js' => 'password_toggle( $( this ).children( "i" ), "password_old" ); return false;'
|
||||
]
|
||||
);?>
|
||||
<?= \Html::input_icon( [
|
||||
'label' => 'Nowe hasło',
|
||||
'name' => 'password_new',
|
||||
'type' => 'password',
|
||||
'inline' => false,
|
||||
'required' => true,
|
||||
'icon_content' => '<i class="fa fa-eye"></i>',
|
||||
'icon_js' => 'password_toggle( $( this ).children( "i" ), "password_new" ); return false;'
|
||||
]
|
||||
);?>
|
||||
<?= \Html::button( [
|
||||
'class' => 'btn-success',
|
||||
'type' => 'submit',
|
||||
'text' => 'Zmień hasło',
|
||||
'icon' => 'fa-check'
|
||||
]
|
||||
);?>
|
||||
</form>
|
||||
<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>
|
||||
</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" />
|
||||
</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-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>
|
||||
|
||||
<script type="text/javascript">
|
||||
function password_toggle( input, id )
|
||||
function password_toggle( btn, id )
|
||||
{
|
||||
$( input ).toggleClass( 'fa-eye' ).toggleClass( 'fa-eye-slash' );
|
||||
if ( $( '#' + id ).attr( 'type' ) === 'password' )
|
||||
$( '#' + id ).attr( 'type', 'text' );
|
||||
var icon = btn.querySelector( 'i' );
|
||||
var input = document.getElementById( id );
|
||||
|
||||
if ( input.type === 'password' )
|
||||
{
|
||||
input.type = 'text';
|
||||
icon.classList.remove( 'fa-eye' );
|
||||
icon.classList.add( 'fa-eye-slash' );
|
||||
}
|
||||
else
|
||||
$( '#' + id ).attr( 'type', 'password' );
|
||||
{
|
||||
input.type = 'password';
|
||||
icon.classList.remove( 'fa-eye-slash' );
|
||||
icon.classList.add( 'fa-eye' );
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user