Add CLI script to fetch active Meta Ads insights for campaigns, adsets, and ads
- Implemented a new PHP script to retrieve insights for the last N days (default 30). - Supports command-line options for token, account ID, days, API version, and output file. - Fetches data at campaign, adset, and ad levels, with filtering for active statuses. - Handles JSON output and optional file saving, including directory creation if necessary. - Includes error handling for cURL requests and JSON responses.
This commit is contained in:
@@ -17,7 +17,11 @@
|
||||
"Bash(git commit:*)",
|
||||
"Bash(php:*)",
|
||||
"WebFetch(domain:adspro.projectpro.pl)",
|
||||
"mcp__ide__getDiagnostics"
|
||||
"mcp__ide__getDiagnostics",
|
||||
"Bash(python3:*)",
|
||||
"Bash(py --version)",
|
||||
"Bash(where:*)",
|
||||
"Bash(python:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class Campaigns
|
||||
foreach ( $db_results as $row )
|
||||
{
|
||||
$result['data'][] = [
|
||||
'<input type="checkbox" class="history-check" value="' . $row['id'] . '">',
|
||||
$row['date_add'],
|
||||
number_format( $row['roas_30_days'], 0, '', ' ' ),
|
||||
number_format( $row['roas_all_time'], 0, '', ' ' ),
|
||||
@@ -192,4 +193,29 @@ class Campaigns
|
||||
echo json_encode( [ 'success' => $result ? true : false ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function delete_history_entries()
|
||||
{
|
||||
$ids = \S::get( 'ids' );
|
||||
|
||||
if ( empty( $ids ) || !is_array( $ids ) )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Nie wybrano wpisow do usuniecia' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$ids = array_map( 'intval', $ids );
|
||||
$ids = array_filter( $ids, function( $id ) { return $id > 0; } );
|
||||
|
||||
if ( empty( $ids ) )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Nie wybrano wpisow do usuniecia' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$deleted = \factory\Campaigns::delete_history_entries( $ids );
|
||||
|
||||
echo json_encode( [ 'success' => true, 'deleted' => $deleted ] );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,48 @@ namespace controls;
|
||||
|
||||
class Clients
|
||||
{
|
||||
static private function get_facebook_conversion_window_days()
|
||||
{
|
||||
global $settings;
|
||||
|
||||
$settings_value = (int) \services\FacebookAdsApi::get_setting( 'facebook_ads_conversion_window_days' );
|
||||
if ( $settings_value > 0 )
|
||||
{
|
||||
return min( 90, $settings_value );
|
||||
}
|
||||
|
||||
$config_value = (int) ( $settings['facebook_ads_conversion_window_days'] ?? 30 );
|
||||
if ( $config_value <= 0 )
|
||||
{
|
||||
$config_value = 30;
|
||||
}
|
||||
|
||||
return min( 90, $config_value );
|
||||
}
|
||||
|
||||
static private function normalize_facebook_ads_account_id( $value )
|
||||
{
|
||||
$value = trim( (string) $value );
|
||||
if ( $value === '' )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( stripos( $value, 'act_' ) === 0 )
|
||||
{
|
||||
$digits = preg_replace( '/\D+/', '', substr( $value, 4 ) );
|
||||
return $digits !== '' ? 'act_' . $digits : null;
|
||||
}
|
||||
|
||||
$digits = preg_replace( '/\D+/', '', $value );
|
||||
if ( $digits === '' )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'act_' . $digits;
|
||||
}
|
||||
|
||||
static private function clients_has_deleted_column()
|
||||
{
|
||||
global $mdb;
|
||||
@@ -61,6 +103,7 @@ class Clients
|
||||
$name = trim( \S::get( 'name' ) );
|
||||
$google_ads_customer_id = trim( \S::get( 'google_ads_customer_id' ) );
|
||||
$google_merchant_account_id = trim( \S::get( 'google_merchant_account_id' ) );
|
||||
$facebook_ads_account_id = self::normalize_facebook_ads_account_id( \S::get( 'facebook_ads_account_id' ) );
|
||||
$active_raw = \S::get( 'active' );
|
||||
$active = (string) $active_raw === '0' ? 0 : 1;
|
||||
|
||||
@@ -77,6 +120,7 @@ class Clients
|
||||
'name' => $name,
|
||||
'google_ads_customer_id' => $google_ads_customer_id ?: null,
|
||||
'google_merchant_account_id' => $google_merchant_account_id ?: null,
|
||||
'facebook_ads_account_id' => $facebook_ads_account_id,
|
||||
'google_ads_start_date' => $google_ads_start_date ?: null,
|
||||
'active' => $active,
|
||||
];
|
||||
@@ -152,6 +196,7 @@ class Clients
|
||||
{
|
||||
global $mdb;
|
||||
$clients_not_deleted_sql = self::sql_clients_not_deleted();
|
||||
$clients_not_deleted_sql_c = self::sql_clients_not_deleted( 'c' );
|
||||
|
||||
// Kampanie: 1 work unit per row (pending=0, done=1)
|
||||
$campaigns_raw = $mdb->query(
|
||||
@@ -205,6 +250,49 @@ class Clients
|
||||
$data[ $merchant_client_id ]['merchant'] = [ $done, 1 ];
|
||||
}
|
||||
|
||||
$facebook_yesterday = date( 'Y-m-d', strtotime( '-1 day' ) );
|
||||
|
||||
try
|
||||
{
|
||||
$facebook_rows = $mdb -> query(
|
||||
"SELECT
|
||||
c.id AS client_id,
|
||||
MAX( h.date_add ) AS last_synced_date
|
||||
FROM clients c
|
||||
LEFT JOIN facebook_campaigns fc
|
||||
ON fc.client_id = c.id
|
||||
LEFT JOIN facebook_campaigns_history h
|
||||
ON h.facebook_campaign_id = fc.id
|
||||
WHERE " . $clients_not_deleted_sql_c . "
|
||||
AND COALESCE( c.active, 0 ) = 1
|
||||
AND TRIM( COALESCE( c.facebook_ads_account_id, '' ) ) <> ''
|
||||
GROUP BY c.id"
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
foreach ( (array) $facebook_rows as $row )
|
||||
{
|
||||
$facebook_client_id = (int) ( $row['client_id'] ?? 0 );
|
||||
if ( $facebook_client_id <= 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$last_synced = (string) ( $row['last_synced_date'] ?? '' );
|
||||
$done = ( $last_synced === $facebook_yesterday ) ? 1 : 0;
|
||||
$data[ $facebook_client_id ]['facebook_ads'] = [ $done, 1 ];
|
||||
}
|
||||
|
||||
$facebook_force_client_id = (int) \services\FacebookAdsApi::get_setting( 'cron_facebook_ads_force_client_id' );
|
||||
if ( $facebook_force_client_id > 0 )
|
||||
{
|
||||
$data[ $facebook_force_client_id ]['facebook_ads'] = [ 0, 1 ];
|
||||
}
|
||||
}
|
||||
catch ( \Throwable $e )
|
||||
{
|
||||
// Tabele Facebook Ads mogly nie byc jeszcze zainstalowane.
|
||||
}
|
||||
|
||||
echo json_encode( [ 'status' => 'ok', 'data' => $data ] );
|
||||
exit;
|
||||
}
|
||||
@@ -225,7 +313,7 @@ class Clients
|
||||
|
||||
$deleted_select = self::clients_has_deleted_column() ? 'COALESCE(deleted, 0) AS deleted' : '0 AS deleted';
|
||||
$client = $mdb -> query(
|
||||
"SELECT id, COALESCE(active, 0) AS active, " . $deleted_select . ", google_ads_customer_id, google_merchant_account_id
|
||||
"SELECT id, COALESCE(active, 0) AS active, " . $deleted_select . ", google_ads_customer_id, google_merchant_account_id, facebook_ads_account_id
|
||||
FROM clients
|
||||
WHERE id = :id
|
||||
LIMIT 1",
|
||||
@@ -274,6 +362,18 @@ class Clients
|
||||
|
||||
\services\GoogleAdsApi::set_setting( 'cron_campaigns_product_alerts_last_client_id', (string) max( 0, $previous_eligible_id ) );
|
||||
}
|
||||
else if ( $pipeline === 'facebook_ads' )
|
||||
{
|
||||
$has_facebook_id = trim( (string) ( $client['facebook_ads_account_id'] ?? '' ) ) !== '';
|
||||
if ( !$has_facebook_id )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Klient nie ma ustawionego Facebook Ads Account ID.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
\services\FacebookAdsApi::set_setting( 'cron_facebook_ads_force_client_id', (string) $id );
|
||||
\services\FacebookAdsApi::set_setting( 'cron_facebook_ads_force_requested_at', date( 'Y-m-d H:i:s' ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
// Domyslny reset (wszystkie pipeline oparte o cron_sync_status).
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
137
autoload/controls/class.FacebookAds.php
Normal file
137
autoload/controls/class.FacebookAds.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
namespace controls;
|
||||
|
||||
class FacebookAds
|
||||
{
|
||||
static private function sanitize_level( $level )
|
||||
{
|
||||
$level = strtolower( trim( (string) $level ) );
|
||||
if ( in_array( $level, [ 'campaign', 'adset', 'ad' ], true ) )
|
||||
{
|
||||
return $level;
|
||||
}
|
||||
|
||||
return 'campaign';
|
||||
}
|
||||
|
||||
static public function main_view()
|
||||
{
|
||||
return \Tpl::view( 'facebook_ads/main_view', [
|
||||
'clients' => \factory\FacebookAds::get_clients_for_reports(),
|
||||
] );
|
||||
}
|
||||
|
||||
static public function get_entities()
|
||||
{
|
||||
$client_id = (int) \S::get( 'client_id' );
|
||||
$level = self::sanitize_level( \S::get( 'level' ) );
|
||||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||||
$ad_set_id = (int) \S::get( 'ad_set_id' );
|
||||
|
||||
echo json_encode( [
|
||||
'level' => $level,
|
||||
'entities' => \factory\FacebookAds::get_entities_with_latest_metrics( $client_id, $level, [
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_set_id' => $ad_set_id
|
||||
] )
|
||||
] );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function get_history_data_table()
|
||||
{
|
||||
$level = self::sanitize_level( \S::get( 'level' ) );
|
||||
$entity_id = (int) \S::get( 'entity_id' );
|
||||
$start = (int) \S::get( 'start' );
|
||||
$length = (int) \S::get( 'length' );
|
||||
|
||||
$rows = \factory\FacebookAds::get_entity_history( $level, $entity_id, $start, $length, false );
|
||||
$records_total = \factory\FacebookAds::get_entity_history_total( $level, $entity_id );
|
||||
|
||||
$result = [
|
||||
'draw' => \S::get( 'draw' ),
|
||||
'recordsTotal' => $records_total,
|
||||
'recordsFiltered' => $records_total,
|
||||
'data' => []
|
||||
];
|
||||
|
||||
$is_campaign = $level === 'campaign';
|
||||
|
||||
foreach ( $rows as $row )
|
||||
{
|
||||
$result['data'][] = [
|
||||
(string) ( $row['date_add'] ?? '' ),
|
||||
number_format( (float) ( $row['spend'] ?? 0 ), 2, ',', ' ' ),
|
||||
(int) ( $row['impressions'] ?? 0 ),
|
||||
(int) ( $row['clicks'] ?? 0 ),
|
||||
number_format( (float) ( $row['ctr'] ?? 0 ), 3, ',', ' ' ),
|
||||
number_format( (float) ( $row['cpc'] ?? 0 ), 3, ',', ' ' ),
|
||||
number_format( (float) ( $row['conversion_value'] ?? 0 ), 2, ',', ' ' ),
|
||||
number_format( (float) ( $row['roas'] ?? 0 ), 2, ',', ' ' ),
|
||||
$is_campaign ? number_format( (float) ( $row['roas_all_time'] ?? 0 ), 2, ',', ' ' ) : ''
|
||||
];
|
||||
}
|
||||
|
||||
$result['is_campaign'] = $is_campaign;
|
||||
|
||||
echo json_encode( $result );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function get_history_data_chart()
|
||||
{
|
||||
$level = self::sanitize_level( \S::get( 'level' ) );
|
||||
$entity_id = (int) \S::get( 'entity_id' );
|
||||
|
||||
$rows = \factory\FacebookAds::get_entity_history( $level, $entity_id, 0, 1000, true );
|
||||
|
||||
$is_campaign = $level === 'campaign';
|
||||
|
||||
$dates = [];
|
||||
$spend = [];
|
||||
$impressions = [];
|
||||
$clicks = [];
|
||||
$ctr = [];
|
||||
$cpc = [];
|
||||
$conversion_value = [];
|
||||
$roas = [];
|
||||
$roas_all_time = [];
|
||||
|
||||
foreach ( $rows as $row )
|
||||
{
|
||||
$dates[] = (string) ( $row['date_add'] ?? '' );
|
||||
$spend[] = (float) ( $row['spend'] ?? 0 );
|
||||
$impressions[] = (int) ( $row['impressions'] ?? 0 );
|
||||
$clicks[] = (int) ( $row['clicks'] ?? 0 );
|
||||
$ctr[] = (float) ( $row['ctr'] ?? 0 );
|
||||
$cpc[] = (float) ( $row['cpc'] ?? 0 );
|
||||
$conversion_value[] = (float) ( $row['conversion_value'] ?? 0 );
|
||||
$roas[] = (float) ( $row['roas'] ?? 0 );
|
||||
if ( $is_campaign )
|
||||
{
|
||||
$roas_all_time[] = (float) ( $row['roas_all_time'] ?? 0 );
|
||||
}
|
||||
}
|
||||
|
||||
$chart_data = [
|
||||
[ 'name' => 'Spend', 'data' => $spend, 'visible' => false ],
|
||||
[ 'name' => 'Impressions', 'data' => $impressions, 'visible' => false ],
|
||||
[ 'name' => 'Clicks', 'data' => $clicks, 'visible' => false ],
|
||||
[ 'name' => 'CTR', 'data' => $ctr, 'visible' => false ],
|
||||
[ 'name' => 'CPC', 'data' => $cpc, 'visible' => false ],
|
||||
[ 'name' => 'Wartosc konwersji', 'data' => $conversion_value, 'visible' => false ],
|
||||
[ 'name' => 'ROAS (30 dni)', 'data' => $roas, 'visible' => true ]
|
||||
];
|
||||
|
||||
if ( $is_campaign )
|
||||
{
|
||||
$chart_data[] = [ 'name' => 'ROAS (all time)', 'data' => $roas_all_time, 'visible' => true ];
|
||||
}
|
||||
|
||||
echo json_encode( [
|
||||
'dates' => $dates,
|
||||
'chart_data' => $chart_data
|
||||
] );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -164,33 +164,6 @@ class Products
|
||||
];
|
||||
}
|
||||
|
||||
static public function get_client_bestseller_min_roas() {
|
||||
$client_id = \S::get( 'client_id' );
|
||||
|
||||
$min_roas = \factory\Products::get_client_bestseller_min_roas( $client_id );
|
||||
|
||||
if ( $min_roas )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'ok', 'min_roas' => $min_roas ] );
|
||||
}
|
||||
else
|
||||
echo json_encode( [ 'status' => 'error' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function save_client_bestseller_min_roas() {
|
||||
$client_id = \S::get( 'client_id' );
|
||||
$min_roas = \S::get( 'min_roas' );
|
||||
|
||||
if ( \factory\Products::save_client_bestseller_min_roas( $client_id, $min_roas ) )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'ok' ] );
|
||||
}
|
||||
else
|
||||
echo json_encode( [ 'status' => 'error' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function main_view()
|
||||
{
|
||||
return \Tpl::view( 'products/main_view', [
|
||||
|
||||
@@ -151,7 +151,7 @@ class Users
|
||||
|
||||
private static function get_cron_dashboard_data()
|
||||
{
|
||||
global $mdb;
|
||||
global $mdb, $settings;
|
||||
|
||||
$base_url = self::get_base_url();
|
||||
$clients_total = (int) $mdb -> query(
|
||||
@@ -167,6 +167,12 @@ class Users
|
||||
AND TRIM( COALESCE( google_ads_customer_id, '' ) ) <> ''
|
||||
AND TRIM( COALESCE( google_merchant_account_id, '' ) ) <> ''"
|
||||
) -> fetchColumn();
|
||||
$facebook_clients_total = (int) $mdb -> query(
|
||||
"SELECT COUNT(*)
|
||||
FROM clients
|
||||
WHERE COALESCE( active, 0 ) = 1
|
||||
AND TRIM( COALESCE( facebook_ads_account_id, '' ) ) <> ''"
|
||||
) -> fetchColumn();
|
||||
|
||||
// --- Kampanie ---
|
||||
$campaign_stats = $mdb -> query(
|
||||
@@ -234,45 +240,98 @@ class Users
|
||||
$products_meta .= ', ' . $products_eta_meta;
|
||||
}
|
||||
|
||||
// --- Walidacja Merchant dla alertow kampanii ---
|
||||
$merchant_cursor_client_id = (int) \services\GoogleAdsApi::get_setting( 'cron_campaigns_product_alerts_last_client_id' );
|
||||
$merchant_processed = 0;
|
||||
if ( $merchant_cursor_client_id > 0 && $merchant_clients_total > 0 )
|
||||
{
|
||||
$merchant_processed = (int) $mdb -> query(
|
||||
"SELECT COUNT(*)
|
||||
FROM clients
|
||||
WHERE COALESCE( active, 0 ) = 1
|
||||
AND TRIM( COALESCE( google_ads_customer_id, '' ) ) <> ''
|
||||
AND TRIM( COALESCE( google_merchant_account_id, '' ) ) <> ''
|
||||
AND id <= :cursor_client_id",
|
||||
[ ':cursor_client_id' => $merchant_cursor_client_id ]
|
||||
) -> fetchColumn();
|
||||
}
|
||||
// --- Merchant: postep na poziomie produktow (URL z Merchant Center) ---
|
||||
$merchant_products_with_offer = (int) $mdb -> query(
|
||||
"SELECT COUNT(*)
|
||||
FROM products p
|
||||
INNER JOIN clients c ON p.client_id = c.id
|
||||
WHERE COALESCE( c.active, 0 ) = 1
|
||||
AND TRIM( COALESCE( c.google_merchant_account_id, '' ) ) <> ''
|
||||
AND TRIM( COALESCE( p.offer_id, '' ) ) <> ''"
|
||||
) -> fetchColumn();
|
||||
|
||||
$merchant_processed = min( $merchant_clients_total, max( 0, $merchant_processed ) );
|
||||
$merchant_remaining = max( 0, $merchant_clients_total - $merchant_processed );
|
||||
$merchant_meta = 'Kursor klienta: ' . ( $merchant_cursor_client_id > 0 ? '#' . $merchant_cursor_client_id : '-' ) . ', klienci z Merchant ID: ' . $merchant_clients_total;
|
||||
$merchant_eta_meta = self::build_eta_meta( 'cron_campaigns_product_alerts_merchant', $merchant_remaining );
|
||||
$merchant_products_with_url = (int) $mdb -> query(
|
||||
"SELECT COUNT(*)
|
||||
FROM products p
|
||||
INNER JOIN clients c ON p.client_id = c.id
|
||||
WHERE COALESCE( c.active, 0 ) = 1
|
||||
AND TRIM( COALESCE( c.google_merchant_account_id, '' ) ) <> ''
|
||||
AND TRIM( COALESCE( p.offer_id, '' ) ) <> ''
|
||||
AND TRIM( COALESCE( p.product_url, '' ) ) <> ''
|
||||
AND LOWER( TRIM( p.product_url ) ) NOT IN ( '0', '-', 'null' )"
|
||||
) -> fetchColumn();
|
||||
|
||||
$merchant_products_not_found = (int) $mdb -> query(
|
||||
"SELECT COUNT(*)
|
||||
FROM products p
|
||||
INNER JOIN clients c ON p.client_id = c.id
|
||||
WHERE COALESCE( c.active, 0 ) = 1
|
||||
AND TRIM( COALESCE( c.google_merchant_account_id, '' ) ) <> ''
|
||||
AND TRIM( COALESCE( p.offer_id, '' ) ) <> ''
|
||||
AND p.product_url IS NULL
|
||||
AND COALESCE( p.merchant_url_not_found, 0 ) = 1"
|
||||
) -> fetchColumn();
|
||||
|
||||
$merchant_processed = $merchant_products_with_url + $merchant_products_not_found;
|
||||
$merchant_total = max( 1, $merchant_products_with_offer );
|
||||
$merchant_remaining_products = max( 0, $merchant_products_with_offer - $merchant_processed );
|
||||
|
||||
$merchant_batch_limit = (int) ( $settings['cron_products_urls_limit_per_client'] ?? 100 );
|
||||
if ( $merchant_batch_limit <= 0 )
|
||||
{
|
||||
$merchant_batch_limit = 100;
|
||||
}
|
||||
$merchant_remaining_calls = ( $merchant_remaining_products > 0 ) ? (int) ceil( $merchant_remaining_products / $merchant_batch_limit ) : 0;
|
||||
|
||||
$merchant_meta = 'Produkty z URL: ' . $merchant_products_with_url
|
||||
. ', brak w MC: ' . $merchant_products_not_found
|
||||
. ', pozostało: ' . $merchant_remaining_products
|
||||
. ', paczka: ' . $merchant_batch_limit . ' szt.'
|
||||
. ', klienci z Merchant ID: ' . $merchant_clients_total;
|
||||
|
||||
$merchant_eta_meta = self::build_eta_meta( 'cron_products_urls', $merchant_remaining_calls );
|
||||
if ( $merchant_eta_meta !== '' )
|
||||
{
|
||||
$merchant_meta .= ', ' . $merchant_eta_meta;
|
||||
}
|
||||
|
||||
// --- Facebook Ads --- (postep = klienci zsynchronizowani dzisiaj)
|
||||
$facebook_yesterday = date( 'Y-m-d', strtotime( '-1 day' ) );
|
||||
$facebook_last_active_date = (string) \services\FacebookAdsApi::get_setting( 'cron_facebook_ads_last_active_date' );
|
||||
$facebook_synced_today = ( $facebook_last_active_date === $facebook_yesterday );
|
||||
|
||||
$facebook_processed = $facebook_synced_today ? $facebook_clients_total : 0;
|
||||
$facebook_total = $facebook_clients_total;
|
||||
$facebook_remaining = $facebook_synced_today ? 0 : 1;
|
||||
|
||||
$facebook_last_success_at = self::format_datetime( \services\FacebookAdsApi::get_setting( 'cron_facebook_ads_last_success_at' ) );
|
||||
$facebook_force_client_id = (int) \services\FacebookAdsApi::get_setting( 'cron_facebook_ads_force_client_id' );
|
||||
|
||||
$facebook_meta = 'Zakres: 30 dni do ' . $facebook_yesterday
|
||||
. ', klienci z FB ID: ' . $facebook_clients_total
|
||||
. ', ostatni sukces: ' . $facebook_last_success_at
|
||||
. ', sync dzisiaj: ' . ( $facebook_synced_today ? 'tak' : 'nie' );
|
||||
if ( $facebook_force_client_id > 0 )
|
||||
{
|
||||
$facebook_meta .= ', zakolejkowany klient: #' . $facebook_force_client_id;
|
||||
}
|
||||
|
||||
$facebook_eta_meta = self::build_eta_meta( 'cron_facebook_ads', $facebook_remaining );
|
||||
if ( $facebook_eta_meta !== '' )
|
||||
{
|
||||
$facebook_meta .= ', ' . $facebook_eta_meta;
|
||||
}
|
||||
|
||||
$cron_schedule = [];
|
||||
|
||||
// --- Endpointy CRON ---
|
||||
$cron_endpoints = [
|
||||
[ 'name' => 'Legacy CRON', 'path' => '/cron.php', 'action' => 'cron_legacy', 'plan' => '' ],
|
||||
[ 'name' => 'Cron zbiorczy (K->P->GMC)', 'path' => '/cron/cron_clients_bundle', 'action' => 'cron_clients_bundle', 'plan' => 'Co 1 min: klient -> kampanie (-7..-1) -> produkty (-7..-1) -> GMC' ],
|
||||
[ 'name' => 'Cron kampanii', 'path' => '/cron/cron_campaigns', 'action' => 'cron_campaigns', 'plan' => 'Krok 1/2, co 15 min' ],
|
||||
[ 'name' => 'Cron alertow kampanii (Merchant)', 'path' => '/cron/cron_campaigns_product_alerts_merchant', 'action' => 'cron_campaigns_product_alerts_merchant', 'plan' => 'Krok 2/2, 2-5 min po Cron kampanii, co 15 min' ],
|
||||
[ 'name' => 'Cron produktów', 'path' => '/cron/cron_products', 'action' => 'cron_products', 'plan' => '' ],
|
||||
[ 'name' => 'Cron uniwersalny (Google Ads)', 'path' => '/cron/cron_universal', 'action' => 'cron_universal', 'plan' => 'Co 1 min: kampanie (wczoraj) + frazy/produkty (7 dni wstecz) + Merchant URL' ],
|
||||
[ 'name' => 'Cron alertow kampanii (Merchant)', 'path' => '/cron/cron_campaigns_product_alerts_merchant', 'action' => 'cron_campaigns_product_alerts_merchant', 'plan' => 'Co 15 min: alerty produktowe z Google Merchant' ],
|
||||
[ 'name' => 'Cron URL produktów (Merchant)', 'path' => '/cron/cron_products_urls', 'action' => 'cron_products_urls', 'plan' => '' ],
|
||||
[ 'name' => 'Cron fraz', 'path' => '/cron/cron_phrases', 'action' => 'cron_phrases', 'plan' => '' ],
|
||||
[ 'name' => 'Historia 30 dni produktów', 'path' => '/cron/cron_products_history_30', 'action' => 'cron_products_history_30', 'plan' => '' ],
|
||||
[ 'name' => 'Historia 30 dni fraz', 'path' => '/cron/cron_phrases_history_30', 'action' => 'cron_phrases_history_30', 'plan' => '' ],
|
||||
[ 'name' => 'Eksport XML', 'path' => '/cron/cron_xml', 'action' => 'cron_xml', 'plan' => '' ],
|
||||
[ 'name' => 'Cron archiwum kampanii', 'path' => '/cron/cron_campaigns_archive', 'action' => 'cron_campaigns_archive', 'plan' => '' ],
|
||||
[ 'name' => 'Cron Facebook Ads', 'path' => '/cron/cron_facebook_ads', 'action' => 'cron_facebook_ads', 'plan' => 'Co 5 min: 30 dni wstecz od wczoraj, blokada ponownego pobrania w tym samym dniu' ],
|
||||
];
|
||||
|
||||
$urls = [];
|
||||
@@ -290,6 +349,7 @@ class Users
|
||||
return [
|
||||
'overall_last_invoked_at' => self::format_datetime( \services\GoogleAdsApi::get_setting( 'cron_last_invoked_at' ) ),
|
||||
'clients_total' => $clients_total,
|
||||
'facebook_clients_total' => $facebook_clients_total,
|
||||
'progress' => [
|
||||
[
|
||||
'name' => 'Kampanie',
|
||||
@@ -306,12 +366,19 @@ class Users
|
||||
'meta' => $products_meta
|
||||
],
|
||||
[
|
||||
'name' => 'Walidacja Merchant',
|
||||
'name' => 'Merchant (URL produktów)',
|
||||
'processed' => $merchant_processed,
|
||||
'total' => $merchant_clients_total,
|
||||
'percent' => self::progress_percent( $merchant_processed, $merchant_clients_total ),
|
||||
'total' => $merchant_total,
|
||||
'percent' => self::progress_percent( $merchant_processed, $merchant_total ),
|
||||
'meta' => $merchant_meta
|
||||
],
|
||||
[
|
||||
'name' => 'Facebook Ads',
|
||||
'processed' => $facebook_processed,
|
||||
'total' => $facebook_total,
|
||||
'percent' => self::progress_percent( $facebook_processed, $facebook_total ),
|
||||
'meta' => $facebook_meta
|
||||
],
|
||||
],
|
||||
'schedule' => $cron_schedule,
|
||||
'urls' => $urls
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
namespace controls;
|
||||
|
||||
class XmlFiles
|
||||
{
|
||||
static public function main_view()
|
||||
{
|
||||
return \Tpl::view( 'xml_files/main_view', [
|
||||
'rows' => \factory\XmlFiles::get_clients_with_xml_feed()
|
||||
] );
|
||||
}
|
||||
|
||||
static public function regenerate()
|
||||
{
|
||||
$client_id = (int) \S::get( 'client_id' );
|
||||
|
||||
if ( $client_id <= 0 )
|
||||
{
|
||||
\S::alert( 'Nie podano ID klienta.' );
|
||||
header( 'Location: /xml_files' );
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = \controls\Cron::generate_custom_feed_for_client( $client_id, true );
|
||||
|
||||
if ( ( $result['status'] ?? '' ) === 'ok' )
|
||||
{
|
||||
\S::alert( 'Plik XML zostal wygenerowany: ' . ( $result['url'] ?? '' ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
\S::alert( $result['message'] ?? 'Nie udalo sie wygenerowac pliku XML.' );
|
||||
}
|
||||
|
||||
header( 'Location: /xml_files' );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -344,4 +344,11 @@ class Campaigns
|
||||
global $mdb;
|
||||
return $mdb -> delete( 'campaigns_history', [ 'id' => $history_id ] );
|
||||
}
|
||||
|
||||
static public function delete_history_entries( $ids )
|
||||
{
|
||||
global $mdb;
|
||||
$mdb -> delete( 'campaigns_history', [ 'id' => $ids ] );
|
||||
return count( $ids );
|
||||
}
|
||||
}
|
||||
|
||||
1014
autoload/factory/class.FacebookAds.php
Normal file
1014
autoload/factory/class.FacebookAds.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -205,18 +205,6 @@ class Products
|
||||
return $mdb -> get( 'products', 'min_roas', [ 'id' => $product_id ] );
|
||||
}
|
||||
|
||||
static public function get_client_bestseller_min_roas( $client_id )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> get( 'clients', 'bestseller_min_roas', [ 'id' => $client_id ] );
|
||||
}
|
||||
|
||||
static public function save_client_bestseller_min_roas( $client_id, $min_roas )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> update( 'clients', [ 'bestseller_min_roas' => $min_roas ], [ 'id' => $client_id ] );
|
||||
}
|
||||
|
||||
static public function save_min_roas( $product_id, $min_roas )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
namespace factory;
|
||||
|
||||
class XmlFiles
|
||||
{
|
||||
static public function get_clients_with_xml_feed()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$clients = $mdb -> query(
|
||||
"SELECT id, name, google_ads_customer_id
|
||||
FROM clients
|
||||
WHERE deleted = 0
|
||||
ORDER BY name ASC"
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$rows = [];
|
||||
|
||||
foreach ( $clients as $client )
|
||||
{
|
||||
$client_id = (int) ( $client['id'] ?? 0 );
|
||||
$scheme = ( !empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? ( $_SERVER['SERVER_NAME'] ?? 'localhost' );
|
||||
$relative_path = '/xml/custom-feed-' . $client_id . '.xml';
|
||||
$absolute_url = $scheme . '://' . $host . $relative_path;
|
||||
$absolute_path = dirname( __DIR__, 2 ) . DIRECTORY_SEPARATOR . 'xml' . DIRECTORY_SEPARATOR . 'custom-feed-' . $client_id . '.xml';
|
||||
$exists = is_file( $absolute_path );
|
||||
$last_modified = $exists ? date( 'Y-m-d H:i:s', (int) filemtime( $absolute_path ) ) : '';
|
||||
|
||||
$rows[] = [
|
||||
'client_id' => $client_id,
|
||||
'client_name' => (string) ( $client['name'] ?? '' ),
|
||||
'google_ads_customer_id' => (string) ( $client['google_ads_customer_id'] ?? '' ),
|
||||
'xml_relative_path' => $relative_path,
|
||||
'xml_url' => $absolute_url,
|
||||
'xml_exists' => $exists,
|
||||
'xml_last_modified' => $last_modified
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
410
autoload/services/class.FacebookAdsApi.php
Normal file
410
autoload/services/class.FacebookAdsApi.php
Normal file
@@ -0,0 +1,410 @@
|
||||
<?php
|
||||
namespace services;
|
||||
|
||||
class FacebookAdsApi
|
||||
{
|
||||
private $access_token;
|
||||
private $api_version;
|
||||
|
||||
public function __construct( $access_token = '', $api_version = 'v25.0' )
|
||||
{
|
||||
$this -> access_token = trim( (string) $access_token );
|
||||
$this -> api_version = trim( (string) $api_version ) ?: 'v25.0';
|
||||
}
|
||||
|
||||
public function is_configured()
|
||||
{
|
||||
return $this -> access_token !== '';
|
||||
}
|
||||
|
||||
public function get_api_version()
|
||||
{
|
||||
return $this -> api_version;
|
||||
}
|
||||
|
||||
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 ] );
|
||||
}
|
||||
|
||||
if ( $key === 'facebook_ads_last_error' )
|
||||
{
|
||||
$error_at = null;
|
||||
if ( $value !== null && trim( (string) $value ) !== '' )
|
||||
{
|
||||
$error_at = date( 'Y-m-d H:i:s' );
|
||||
}
|
||||
|
||||
if ( $mdb -> count( 'settings', [ 'setting_key' => 'facebook_ads_last_error_at' ] ) )
|
||||
{
|
||||
$mdb -> update( 'settings', [ 'setting_value' => $error_at ], [ 'setting_key' => 'facebook_ads_last_error_at' ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$mdb -> insert( 'settings', [ 'setting_key' => 'facebook_ads_last_error_at', 'setting_value' => $error_at ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function normalize_ad_account_id( $account_id )
|
||||
{
|
||||
$account_id = trim( (string) $account_id );
|
||||
if ( $account_id === '' )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( stripos( $account_id, 'act_' ) === 0 )
|
||||
{
|
||||
$digits = preg_replace( '/\D+/', '', substr( $account_id, 4 ) );
|
||||
return $digits !== '' ? 'act_' . $digits : null;
|
||||
}
|
||||
|
||||
$digits = preg_replace( '/\D+/', '', $account_id );
|
||||
if ( $digits === '' )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'act_' . $digits;
|
||||
}
|
||||
|
||||
public function fetch_active_insights_last_days( $account_id, $days = 30, $active_only = true )
|
||||
{
|
||||
$days = max( 1, min( 90, (int) $days ) );
|
||||
$since = date( 'Y-m-d', strtotime( '-' . ( $days - 1 ) . ' days' ) );
|
||||
$until = date( 'Y-m-d' );
|
||||
|
||||
return $this -> fetch_active_insights_range( $account_id, $since, $until, $active_only, $days );
|
||||
}
|
||||
|
||||
public function fetch_active_insights_for_date( $account_id, $date, $active_only = true )
|
||||
{
|
||||
$timestamp = strtotime( (string) $date );
|
||||
if ( !$timestamp )
|
||||
{
|
||||
self::set_setting( 'facebook_ads_last_error', 'Niepoprawna data dla synchronizacji Facebook Ads.' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$sync_date = date( 'Y-m-d', $timestamp );
|
||||
return $this -> fetch_active_insights_range( $account_id, $sync_date, $sync_date, $active_only, 1 );
|
||||
}
|
||||
|
||||
public function fetch_active_insights_for_range( $account_id, $since, $until, $active_only = true )
|
||||
{
|
||||
$since_ts = strtotime( (string) $since );
|
||||
$until_ts = strtotime( (string) $until );
|
||||
if ( !$since_ts || !$until_ts )
|
||||
{
|
||||
self::set_setting( 'facebook_ads_last_error', 'Niepoprawny zakres dat dla synchronizacji Facebook Ads.' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$days = (int) floor( ( $until_ts - $since_ts ) / 86400 ) + 1;
|
||||
return $this -> fetch_active_insights_range( $account_id, date( 'Y-m-d', $since_ts ), date( 'Y-m-d', $until_ts ), $active_only, $days, true );
|
||||
}
|
||||
|
||||
public function fetch_campaigns_all_time( $account_id, $active_only = true )
|
||||
{
|
||||
$account_id = self::normalize_ad_account_id( $account_id );
|
||||
if ( !$account_id )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !$this -> is_configured() )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$base_url = 'https://graph.facebook.com/' . rawurlencode( $this -> api_version ) . '/' . rawurlencode( $account_id ) . '/insights';
|
||||
|
||||
$params = [
|
||||
'access_token' => $this -> access_token,
|
||||
'level' => 'campaign',
|
||||
'fields' => 'campaign_id,spend,action_values,purchase_roas',
|
||||
'date_preset' => 'maximum',
|
||||
'limit' => 500
|
||||
];
|
||||
|
||||
if ( $active_only )
|
||||
{
|
||||
$params['filtering'] = json_encode( [
|
||||
[
|
||||
'field' => 'campaign.effective_status',
|
||||
'operator' => 'IN',
|
||||
'value' => [ 'ACTIVE' ]
|
||||
]
|
||||
] );
|
||||
}
|
||||
|
||||
$rows = $this -> fetch_all_pages( $base_url, $params );
|
||||
if ( $rows === false )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$purchase_action_types = [
|
||||
'purchase', 'omni_purchase', 'offsite_conversion.fb_pixel_purchase',
|
||||
'web_in_store_purchase', 'onsite_conversion.purchase', 'app_custom_event.fb_mobile_purchase'
|
||||
];
|
||||
|
||||
$campaigns = [];
|
||||
foreach ( $rows as $row )
|
||||
{
|
||||
$campaign_id = (string) ( $row['campaign_id'] ?? '' );
|
||||
if ( $campaign_id === '' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$spend = (float) ( $row['spend'] ?? 0 );
|
||||
|
||||
// 1. ROAS z purchase_roas (preferowane)
|
||||
$roas = 0.0;
|
||||
if ( isset( $row['purchase_roas'] ) && is_array( $row['purchase_roas'] ) )
|
||||
{
|
||||
foreach ( $row['purchase_roas'] as $pr )
|
||||
{
|
||||
$at = trim( (string) ( $pr['action_type'] ?? '' ) );
|
||||
if ( in_array( $at, $purchase_action_types, true ) )
|
||||
{
|
||||
$roas = round( (float) ( $pr['value'] ?? 0 ) * 100, 6 );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback: oblicz z action_values / spend
|
||||
$conversion_value = 0.0;
|
||||
if ( isset( $row['action_values'] ) && is_array( $row['action_values'] ) )
|
||||
{
|
||||
foreach ( $row['action_values'] as $action )
|
||||
{
|
||||
$at = trim( (string) ( $action['action_type'] ?? '' ) );
|
||||
if ( in_array( $at, $purchase_action_types, true ) )
|
||||
{
|
||||
$conversion_value += (float) ( $action['value'] ?? 0 );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $roas <= 0 && $spend > 0 && $conversion_value > 0 )
|
||||
{
|
||||
$roas = round( ( $conversion_value / $spend ) * 100, 6 );
|
||||
}
|
||||
|
||||
$campaigns[ $campaign_id ] = [
|
||||
'campaign_id' => $campaign_id,
|
||||
'spend_all_time' => $spend,
|
||||
'conversion_value_all_time' => $conversion_value,
|
||||
'roas_all_time' => $roas
|
||||
];
|
||||
}
|
||||
|
||||
return $campaigns;
|
||||
}
|
||||
|
||||
private function fetch_active_insights_range( $account_id, $since, $until, $active_only = true, $days = 0, $aggregate = false )
|
||||
{
|
||||
$account_id = self::normalize_ad_account_id( $account_id );
|
||||
if ( !$account_id )
|
||||
{
|
||||
self::set_setting( 'facebook_ads_last_error', 'Niepoprawne Facebook Ads Account ID.' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !$this -> is_configured() )
|
||||
{
|
||||
self::set_setting( 'facebook_ads_last_error', 'Brak tokena Facebook Ads API.' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$since_ts = strtotime( (string) $since );
|
||||
$until_ts = strtotime( (string) $until );
|
||||
if ( !$since_ts || !$until_ts || $since_ts > $until_ts )
|
||||
{
|
||||
self::set_setting( 'facebook_ads_last_error', 'Niepoprawny zakres dat Facebook Ads.' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$since = date( 'Y-m-d', $since_ts );
|
||||
$until = date( 'Y-m-d', $until_ts );
|
||||
if ( (int) $days <= 0 )
|
||||
{
|
||||
$days = (int) floor( ( $until_ts - $since_ts ) / 86400 ) + 1;
|
||||
}
|
||||
$days = max( 1, min( 90, (int) $days ) );
|
||||
|
||||
$base_url = 'https://graph.facebook.com/' . rawurlencode( $this -> api_version ) . '/' . rawurlencode( $account_id ) . '/insights';
|
||||
|
||||
$levels = [
|
||||
'campaign' => [
|
||||
'fields' => 'account_id,campaign_id,campaign_name,spend,impressions,clicks,ctr,cpc,action_values,purchase_roas,date_start,date_stop',
|
||||
'filtering_field' => 'campaign.effective_status'
|
||||
],
|
||||
'adset' => [
|
||||
'fields' => 'account_id,campaign_id,campaign_name,adset_id,adset_name,spend,impressions,clicks,ctr,cpc,action_values,purchase_roas,date_start,date_stop',
|
||||
'filtering_field' => 'adset.effective_status'
|
||||
],
|
||||
'ad' => [
|
||||
'fields' => 'account_id,campaign_id,campaign_name,adset_id,adset_name,ad_id,ad_name,spend,impressions,clicks,ctr,cpc,action_values,purchase_roas,date_start,date_stop',
|
||||
'filtering_field' => 'ad.effective_status'
|
||||
]
|
||||
];
|
||||
|
||||
$result = [
|
||||
'meta' => [
|
||||
'account_id' => $account_id,
|
||||
'api_version' => $this -> api_version,
|
||||
'days' => $days,
|
||||
'since' => $since,
|
||||
'until' => $until,
|
||||
'generated_at' => date( 'c' )
|
||||
],
|
||||
'campaign' => [],
|
||||
'adset' => [],
|
||||
'ad' => []
|
||||
];
|
||||
|
||||
foreach ( $levels as $level => $cfg )
|
||||
{
|
||||
$params = [
|
||||
'access_token' => $this -> access_token,
|
||||
'level' => $level,
|
||||
'fields' => $cfg['fields'],
|
||||
'time_range' => json_encode( [ 'since' => $since, 'until' => $until ] ),
|
||||
'limit' => 500
|
||||
];
|
||||
|
||||
if ( !$aggregate )
|
||||
{
|
||||
$params['time_increment'] = 1;
|
||||
}
|
||||
|
||||
if ( $active_only )
|
||||
{
|
||||
$params['filtering'] = json_encode( [
|
||||
[
|
||||
'field' => $cfg['filtering_field'],
|
||||
'operator' => 'IN',
|
||||
'value' => [ 'ACTIVE' ]
|
||||
]
|
||||
] );
|
||||
}
|
||||
|
||||
$rows = $this -> fetch_all_pages( $base_url, $params );
|
||||
if ( $rows === false )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$result[ $level ] = $rows;
|
||||
}
|
||||
|
||||
self::set_setting( 'facebook_ads_last_error', null );
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function fetch_all_pages( $url, $params = null )
|
||||
{
|
||||
$all_rows = [];
|
||||
$next_url = $url;
|
||||
$next_params = $params;
|
||||
|
||||
while ( $next_url )
|
||||
{
|
||||
$payload = $this -> request_json( $next_url, $next_params );
|
||||
if ( $payload === false )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( isset( $payload['data'] ) && is_array( $payload['data'] ) )
|
||||
{
|
||||
foreach ( $payload['data'] as $row )
|
||||
{
|
||||
$all_rows[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$next_url = '';
|
||||
$next_params = null;
|
||||
|
||||
if ( isset( $payload['paging']['next'] ) && is_string( $payload['paging']['next'] ) )
|
||||
{
|
||||
$next_url = $payload['paging']['next'];
|
||||
}
|
||||
}
|
||||
|
||||
return $all_rows;
|
||||
}
|
||||
|
||||
private function request_json( $url, $params = null )
|
||||
{
|
||||
if ( is_array( $params ) )
|
||||
{
|
||||
$query = http_build_query( $params );
|
||||
$url .= ( strpos( $url, '?' ) === false ? '?' : '&' ) . $query;
|
||||
}
|
||||
|
||||
$ch = curl_init( $url );
|
||||
curl_setopt_array( $ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 20,
|
||||
CURLOPT_TIMEOUT => 120
|
||||
] );
|
||||
|
||||
$response = curl_exec( $ch );
|
||||
$http_code = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||
$curl_error = curl_error( $ch );
|
||||
curl_close( $ch );
|
||||
|
||||
if ( $response === false )
|
||||
{
|
||||
self::set_setting( 'facebook_ads_last_error', 'cURL error: ' . $curl_error );
|
||||
return false;
|
||||
}
|
||||
|
||||
$decoded = json_decode( (string) $response, true );
|
||||
if ( !is_array( $decoded ) )
|
||||
{
|
||||
self::set_setting( 'facebook_ads_last_error', 'Niepoprawny JSON odpowiedzi Meta API. HTTP ' . $http_code );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( isset( $decoded['error'] ) )
|
||||
{
|
||||
$message = (string) ( $decoded['error']['message'] ?? 'Nieznany blad Meta API' );
|
||||
$code = (string) ( $decoded['error']['code'] ?? '' );
|
||||
$subcode = (string) ( $decoded['error']['error_subcode'] ?? '' );
|
||||
self::set_setting( 'facebook_ads_last_error', 'Meta API: ' . $message . ' (code: ' . $code . ', subcode: ' . $subcode . ')' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $http_code >= 400 )
|
||||
{
|
||||
self::set_setting( 'facebook_ads_last_error', 'Meta API HTTP ' . $http_code . ': ' . substr( (string) $response, 0, 1000 ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,17 @@ class GoogleAdsApi
|
||||
{
|
||||
$mdb -> insert( 'settings', [ 'setting_key' => $key, 'setting_value' => $value ] );
|
||||
}
|
||||
|
||||
if ( $key === 'google_ads_last_error' )
|
||||
{
|
||||
$error_at = null;
|
||||
if ( $value !== null && trim( (string) $value ) !== '' )
|
||||
{
|
||||
$error_at = date( 'Y-m-d H:i:s' );
|
||||
}
|
||||
|
||||
self::set_setting( 'google_ads_last_error_at', $error_at );
|
||||
}
|
||||
}
|
||||
|
||||
// --- Konfiguracja ---
|
||||
@@ -567,7 +578,7 @@ class GoogleAdsApi
|
||||
|
||||
if ( $http_code !== 200 || !$response )
|
||||
{
|
||||
self::set_setting( 'google_ads_last_error', 'searchStream failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . substr( $response, 0, 500 ) );
|
||||
self::set_setting( 'google_ads_last_error', 'searchStream failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . (string) $response );
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -634,7 +645,7 @@ class GoogleAdsApi
|
||||
|
||||
if ( $http_code !== 200 || !$response )
|
||||
{
|
||||
self::set_setting( 'google_ads_last_error', 'mutate failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . substr( (string) $response, 0, 1000 ) );
|
||||
self::set_setting( 'google_ads_last_error', 'mutate failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . (string) $response );
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1498,7 +1509,7 @@ class GoogleAdsApi
|
||||
|
||||
if ( $http_code !== 200 || !$response )
|
||||
{
|
||||
self::set_setting( 'google_ads_last_error', 'generateKeywordIdeas failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . substr( (string) $response, 0, 1000 ) );
|
||||
self::set_setting( 'google_ads_last_error', 'generateKeywordIdeas failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . (string) $response );
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2609,7 +2620,7 @@ class GoogleAdsApi
|
||||
}
|
||||
|
||||
$last_error = (string) self::get_setting( 'google_ads_last_error' );
|
||||
$variant_errors[] = 'V' . ( $index + 1 ) . ': ' . substr( $last_error, 0, 350 );
|
||||
$variant_errors[] = 'V' . ( $index + 1 ) . ': ' . $last_error;
|
||||
}
|
||||
|
||||
if ( !is_array( $results ) )
|
||||
|
||||
@@ -14,3 +14,7 @@ $settings['cron_products_clients_per_run'] = 1;
|
||||
$settings['cron_campaigns_clients_per_run'] = 1;
|
||||
$settings['cron_products_urls_limit_per_client'] = 100;
|
||||
$settings['google_ads_conversion_window_days'] = 7;
|
||||
|
||||
|
||||
$settings['facebook_ads_token'] = 'EAAVtpObUlr8BQ4sZBbRNOMFTMrSF2PhzOT9vZAFJZAX0xDz5NLlJxECbNmUT5cYLOM0UtH6QhH0OjmkYZAdJgYZBTUZA3tSw2KD9cjsDFMRUB7ReZBVcJBZCbwG8H51sckgfDIWaFRn2Hp2VdddC7hjDP5oY50krI0lUFxwcN08axIr3XUrxpydYZBfvlJl40cwZDZD';
|
||||
$settings['facebook_ads_conversion_window_days'] = 7;
|
||||
@@ -395,3 +395,32 @@
|
||||
|
||||
- Etykieta `custom_label_4` jest czytana i zapisywana z tabeli `products`.
|
||||
- Agregaty (`products_aggregate`) nie sa zrodlem dla pola `custom_label_4`.
|
||||
|
||||
# 2026-02-20 - Usuniecie funkcjonalnosci `bestseller_min_roas`
|
||||
|
||||
## Zmienione pliki
|
||||
|
||||
- `templates/products/main_view.php`
|
||||
- Usuniety filtr UI: pole `Bestseller min ROAS` (`#bestseller_min_roas`).
|
||||
- Usuniety frontendowy loader wartosci progu (`load_client_bestseller_min_roas`).
|
||||
- Usuniete wywolania loadera przy zmianie klienta i przy inicjalizacji strony.
|
||||
- Usuniety zapis AJAX progu klienta na blur (`/products/save_client_bestseller_min_roas/`).
|
||||
|
||||
- `autoload/controls/class.Products.php`
|
||||
- Usuniete endpointy:
|
||||
- `get_client_bestseller_min_roas()`
|
||||
- `save_client_bestseller_min_roas()`
|
||||
|
||||
- `autoload/factory/class.Products.php`
|
||||
- Usuniete metody dostepu do progu ROAS klienta:
|
||||
- `get_client_bestseller_min_roas( $client_id )`
|
||||
- `save_client_bestseller_min_roas( $client_id, $min_roas )`
|
||||
|
||||
- `autoload/controls/class.Cron.php`
|
||||
- W `rebuild_products_temp_for_client()` usunieta logika automatycznej zmiany `custom_label_4` oparta o prog `bestseller_min_roas`.
|
||||
- Funkcja pozostaje jako krok diagnostyczny zwracajacy liczbe scope z `products_aggregate`.
|
||||
|
||||
## Efekt
|
||||
|
||||
- Aplikacja nie odczytuje, nie zapisuje i nie wykorzystuje juz `bestseller_min_roas` w UI, endpointach ani w CRON.
|
||||
- Automatyczne oznaczanie `custom_label_4 = bestseller` na podstawie tego progu zostalo wycofane.
|
||||
|
||||
136
migrations/020_facebook_ads_base.sql
Normal file
136
migrations/020_facebook_ads_base.sql
Normal file
@@ -0,0 +1,136 @@
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'clients'
|
||||
AND COLUMN_NAME = 'facebook_ads_account_id'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `clients` ADD COLUMN `facebook_ads_account_id` VARCHAR(40) NULL DEFAULT NULL AFTER `google_ads_customer_id`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `facebook_campaigns` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`client_id` INT(11) NOT NULL,
|
||||
`account_id` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`campaign_id` BIGINT(20) UNSIGNED NOT NULL,
|
||||
`campaign_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`status` VARCHAR(40) NULL DEFAULT NULL,
|
||||
`effective_status` VARCHAR(40) NULL DEFAULT NULL,
|
||||
`date_sync` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_facebook_campaigns_client_campaign` (`client_id`, `campaign_id`),
|
||||
KEY `idx_facebook_campaigns_client_id` (`client_id`),
|
||||
KEY `idx_facebook_campaigns_campaign_id` (`campaign_id`),
|
||||
CONSTRAINT `FK_facebook_campaigns_clients`
|
||||
FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `facebook_campaigns_history` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`facebook_campaign_id` INT(11) NOT NULL,
|
||||
`spend` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`impressions` INT(11) NOT NULL DEFAULT 0,
|
||||
`clicks` INT(11) NOT NULL DEFAULT 0,
|
||||
`ctr` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`cpc` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`date_add` DATE NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_facebook_campaigns_history_day` (`facebook_campaign_id`, `date_add`),
|
||||
KEY `idx_facebook_campaigns_history_date_add` (`date_add`),
|
||||
CONSTRAINT `FK_facebook_campaigns_history_campaigns`
|
||||
FOREIGN KEY (`facebook_campaign_id`) REFERENCES `facebook_campaigns` (`id`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `facebook_ad_sets` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`client_id` INT(11) NOT NULL,
|
||||
`facebook_campaign_id` INT(11) NULL DEFAULT NULL,
|
||||
`campaign_id` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`ad_set_id` BIGINT(20) UNSIGNED NOT NULL,
|
||||
`ad_set_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`status` VARCHAR(40) NULL DEFAULT NULL,
|
||||
`effective_status` VARCHAR(40) NULL DEFAULT NULL,
|
||||
`date_sync` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_facebook_ad_sets_client_ad_set` (`client_id`, `ad_set_id`),
|
||||
KEY `idx_facebook_ad_sets_client_id` (`client_id`),
|
||||
KEY `idx_facebook_ad_sets_facebook_campaign_id` (`facebook_campaign_id`),
|
||||
KEY `idx_facebook_ad_sets_campaign_id` (`campaign_id`),
|
||||
CONSTRAINT `FK_facebook_ad_sets_clients`
|
||||
FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `FK_facebook_ad_sets_campaigns`
|
||||
FOREIGN KEY (`facebook_campaign_id`) REFERENCES `facebook_campaigns` (`id`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `facebook_ad_sets_history` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`facebook_ad_set_id` INT(11) NOT NULL,
|
||||
`spend` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`impressions` INT(11) NOT NULL DEFAULT 0,
|
||||
`clicks` INT(11) NOT NULL DEFAULT 0,
|
||||
`ctr` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`cpc` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`date_add` DATE NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_facebook_ad_sets_history_day` (`facebook_ad_set_id`, `date_add`),
|
||||
KEY `idx_facebook_ad_sets_history_date_add` (`date_add`),
|
||||
CONSTRAINT `FK_facebook_ad_sets_history_ad_sets`
|
||||
FOREIGN KEY (`facebook_ad_set_id`) REFERENCES `facebook_ad_sets` (`id`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `facebook_ads` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`client_id` INT(11) NOT NULL,
|
||||
`facebook_campaign_id` INT(11) NULL DEFAULT NULL,
|
||||
`facebook_ad_set_id` INT(11) NULL DEFAULT NULL,
|
||||
`campaign_id` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`ad_set_id` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`ad_id` BIGINT(20) UNSIGNED NOT NULL,
|
||||
`ad_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`status` VARCHAR(40) NULL DEFAULT NULL,
|
||||
`effective_status` VARCHAR(40) NULL DEFAULT NULL,
|
||||
`date_sync` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_facebook_ads_client_ad` (`client_id`, `ad_id`),
|
||||
KEY `idx_facebook_ads_client_id` (`client_id`),
|
||||
KEY `idx_facebook_ads_facebook_campaign_id` (`facebook_campaign_id`),
|
||||
KEY `idx_facebook_ads_facebook_ad_set_id` (`facebook_ad_set_id`),
|
||||
KEY `idx_facebook_ads_campaign_id` (`campaign_id`),
|
||||
KEY `idx_facebook_ads_ad_set_id` (`ad_set_id`),
|
||||
CONSTRAINT `FK_facebook_ads_clients`
|
||||
FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `FK_facebook_ads_campaigns`
|
||||
FOREIGN KEY (`facebook_campaign_id`) REFERENCES `facebook_campaigns` (`id`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `FK_facebook_ads_ad_sets`
|
||||
FOREIGN KEY (`facebook_ad_set_id`) REFERENCES `facebook_ad_sets` (`id`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `facebook_ads_history` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`facebook_ad_id` INT(11) NOT NULL,
|
||||
`spend` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`impressions` INT(11) NOT NULL DEFAULT 0,
|
||||
`clicks` INT(11) NOT NULL DEFAULT 0,
|
||||
`ctr` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`cpc` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`date_add` DATE NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_facebook_ads_history_day` (`facebook_ad_id`, `date_add`),
|
||||
KEY `idx_facebook_ads_history_date_add` (`date_add`),
|
||||
CONSTRAINT `FK_facebook_ads_history_ads`
|
||||
FOREIGN KEY (`facebook_ad_id`) REFERENCES `facebook_ads` (`id`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
91
migrations/021_facebook_ads_conversion_metrics.sql
Normal file
91
migrations/021_facebook_ads_conversion_metrics.sql
Normal file
@@ -0,0 +1,91 @@
|
||||
-- Metryki konwersji i ROAS dla historii Facebook Ads
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'facebook_campaigns_history'
|
||||
AND COLUMN_NAME = 'conversion_value'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `facebook_campaigns_history` ADD COLUMN `conversion_value` DECIMAL(20,6) NOT NULL DEFAULT 0.000000 AFTER `cpc`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'facebook_campaigns_history'
|
||||
AND COLUMN_NAME = 'roas'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `facebook_campaigns_history` ADD COLUMN `roas` DECIMAL(20,6) NOT NULL DEFAULT 0.000000 AFTER `conversion_value`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'facebook_ad_sets_history'
|
||||
AND COLUMN_NAME = 'conversion_value'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `facebook_ad_sets_history` ADD COLUMN `conversion_value` DECIMAL(20,6) NOT NULL DEFAULT 0.000000 AFTER `cpc`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'facebook_ad_sets_history'
|
||||
AND COLUMN_NAME = 'roas'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `facebook_ad_sets_history` ADD COLUMN `roas` DECIMAL(20,6) NOT NULL DEFAULT 0.000000 AFTER `conversion_value`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'facebook_ads_history'
|
||||
AND COLUMN_NAME = 'conversion_value'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `facebook_ads_history` ADD COLUMN `conversion_value` DECIMAL(20,6) NOT NULL DEFAULT 0.000000 AFTER `cpc`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'facebook_ads_history'
|
||||
AND COLUMN_NAME = 'roas'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `facebook_ads_history` ADD COLUMN `roas` DECIMAL(20,6) NOT NULL DEFAULT 0.000000 AFTER `conversion_value`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
16
migrations/022_facebook_ads_roas_all_time.sql
Normal file
16
migrations/022_facebook_ads_roas_all_time.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- ROAS all time dla historii kampanii Facebook Ads
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'facebook_campaigns_history'
|
||||
AND COLUMN_NAME = 'roas_all_time'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `facebook_campaigns_history` ADD COLUMN `roas_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000 AFTER `roas`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
121
temp_fb_authentication.html
Normal file
121
temp_fb_authentication.html
Normal file
File diff suppressed because one or more lines are too long
209
temp_fb_authorization.html
Normal file
209
temp_fb_authorization.html
Normal file
File diff suppressed because one or more lines are too long
49
temp_fb_get_started.html
Normal file
49
temp_fb_get_started.html
Normal file
File diff suppressed because one or more lines are too long
303
temp_fb_insights_async.html
Normal file
303
temp_fb_insights_async.html
Normal file
File diff suppressed because one or more lines are too long
125
temp_fb_system_users.html
Normal file
125
temp_fb_system_users.html
Normal file
File diff suppressed because one or more lines are too long
@@ -1733,11 +1733,16 @@ function load_campaigns_for_client( restore_campaign_id )
|
||||
var data = JSON.parse( response );
|
||||
var campaigns = Object.entries( data.campaigns || {} );
|
||||
|
||||
campaigns = campaigns.filter( function( pair )
|
||||
{
|
||||
var row = pair[1] || {};
|
||||
var campaign_name = String( row.campaign_name || '' ).toLowerCase().trim();
|
||||
return campaign_name !== '--- konto ---';
|
||||
} );
|
||||
|
||||
campaigns.sort( function( a, b ) {
|
||||
var nameA = String( ( a[1] && a[1].campaign_name ) ? a[1].campaign_name : '' ).toLowerCase();
|
||||
var nameB = String( ( b[1] && b[1].campaign_name ) ? b[1].campaign_name : '' ).toLowerCase();
|
||||
if ( nameA === '--- konto ---' ) return -1;
|
||||
if ( nameB === '--- konto ---' ) return 1;
|
||||
if ( nameA > nameB ) return 1;
|
||||
if ( nameA < nameB ) return -1;
|
||||
return 0;
|
||||
|
||||
@@ -36,9 +36,15 @@
|
||||
</div>
|
||||
|
||||
<div class="campaigns-table-wrap">
|
||||
<div id="history_bulk_actions" style="display:none; margin-bottom: 10px;">
|
||||
<button type="button" id="delete_history_entries" class="btn btn-danger btn-sm">
|
||||
<i class="fa-solid fa-trash"></i> Usun zaznaczone (<span id="history_selected_count">0</span>)
|
||||
</button>
|
||||
</div>
|
||||
<table class="table" id="products">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 30px; text-align: center;"><input type="checkbox" id="select_all_history"></th>
|
||||
<th>Data</th>
|
||||
<th>ROAS (30 dni)</th>
|
||||
<th>ROAS (all time)</th>
|
||||
@@ -468,6 +474,8 @@ $( function()
|
||||
$( '#products' ).DataTable().destroy();
|
||||
$( '#products tbody' ).empty();
|
||||
}
|
||||
$( '#history_bulk_actions' ).hide();
|
||||
$( '#select_all_history' ).prop( 'checked', false );
|
||||
|
||||
if ( vals.length === 1 )
|
||||
{
|
||||
@@ -485,6 +493,7 @@ $( function()
|
||||
lengthChange: false,
|
||||
pageLength: 15,
|
||||
columns: [
|
||||
{ width: '30px', orderable: false, className: 'dt-center', searchable: false },
|
||||
{ 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" },
|
||||
@@ -518,6 +527,93 @@ $( function()
|
||||
}
|
||||
});
|
||||
|
||||
// --- Checkboxy historii: select all, licznik, bulk delete ---
|
||||
|
||||
function updateHistoryBulkActions()
|
||||
{
|
||||
var checked = $( '#products .history-check:checked' );
|
||||
var count = checked.length;
|
||||
$( '#history_selected_count' ).text( count );
|
||||
$( '#history_bulk_actions' ).toggle( count > 0 );
|
||||
}
|
||||
|
||||
$( 'body' ).on( 'change', '#select_all_history', function()
|
||||
{
|
||||
var isChecked = this.checked;
|
||||
$( '#products .history-check' ).prop( 'checked', isChecked );
|
||||
updateHistoryBulkActions();
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'change', '.history-check', function()
|
||||
{
|
||||
var all = $( '#products .history-check' );
|
||||
var checked = $( '#products .history-check:checked' );
|
||||
$( '#select_all_history' ).prop( 'checked', all.length > 0 && all.length === checked.length );
|
||||
updateHistoryBulkActions();
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'draw.dt', '#products', function()
|
||||
{
|
||||
$( '#select_all_history' ).prop( 'checked', false );
|
||||
updateHistoryBulkActions();
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'click', '#delete_history_entries', function()
|
||||
{
|
||||
var ids = [];
|
||||
$( '#products .history-check:checked' ).each( function() {
|
||||
ids.push( $( this ).val() );
|
||||
});
|
||||
|
||||
if ( !ids.length ) return;
|
||||
|
||||
$.confirm({
|
||||
title: 'Potwierdzenie usuniecia',
|
||||
content: 'Czy na pewno chcesz usunac <strong>' + ids.length + '</strong> ' + ( ids.length === 1 ? 'wpis' : 'wpisow' ) + ' z historii?<br>Ta operacja jest nieodwracalna.',
|
||||
type: 'red',
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: 'Usun (' + ids.length + ')',
|
||||
btnClass: 'btn-red',
|
||||
keys: ['enter'],
|
||||
action: function()
|
||||
{
|
||||
$.ajax({
|
||||
url: '/campaigns/delete_history_entries/',
|
||||
type: 'POST',
|
||||
data: { ids: ids },
|
||||
success: function( response )
|
||||
{
|
||||
var data = JSON.parse( response );
|
||||
if ( data.success )
|
||||
{
|
||||
$.alert({
|
||||
title: 'Sukces',
|
||||
content: 'Usunieto ' + data.deleted + ' ' + ( data.deleted === 1 ? 'wpis' : 'wpisow' ) + '.',
|
||||
type: 'green',
|
||||
autoClose: 'ok|2000'
|
||||
});
|
||||
if ( $.fn.DataTable.isDataTable( '#products' ) )
|
||||
$( '#products' ).DataTable().ajax.reload( null, false );
|
||||
reloadChart();
|
||||
}
|
||||
else
|
||||
{
|
||||
$.alert({
|
||||
title: 'Blad',
|
||||
content: data.message || 'Nie udalo sie usunac wpisow.',
|
||||
type: 'red'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
cancel: { text: 'Anuluj' }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var saved_client_id = storage_get( STORAGE_CLIENT_KEY );
|
||||
var saved_campaign_id = storage_get( STORAGE_CAMPAIGN_KEY );
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<th>Nazwa klienta</th>
|
||||
<th style="width: 120px;">Status</th>
|
||||
<th>Google Ads Customer ID</th>
|
||||
<th>Facebook Ads Account ID</th>
|
||||
<th>Merchant Account ID</th>
|
||||
<th>Dane od</th>
|
||||
<th style="width: 190px;">Sync</th>
|
||||
@@ -41,6 +42,13 @@
|
||||
<span class="text-muted">— brak —</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ( !empty( $client['facebook_ads_account_id'] ) ): ?>
|
||||
<span class="badge-id"><?= htmlspecialchars( $client['facebook_ads_account_id'] ); ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">— brak —</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ( !empty( $client['google_merchant_account_id'] ) ): ?>
|
||||
<span class="badge-id"><?= htmlspecialchars( $client['google_merchant_account_id'] ); ?></span>
|
||||
@@ -73,6 +81,11 @@
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php if ( !empty( $client['facebook_ads_account_id'] ) ): ?>
|
||||
<button type="button" class="btn-icon btn-icon-sync client-sync-action" onclick="syncClient(<?= $client['id']; ?>, 'facebook_ads', this)" title="Odswiez Facebook Ads" <?= $is_client_active ? '' : 'disabled'; ?>>
|
||||
<i class="fa-brands fa-facebook-f"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn-icon btn-icon-edit" onclick="editClient(<?= $client['id']; ?>)" title="Edytuj">
|
||||
<i class="fa-solid fa-pen"></i>
|
||||
</button>
|
||||
@@ -84,7 +97,7 @@
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="8" class="empty-state">
|
||||
<td colspan="9" class="empty-state">
|
||||
<i class="fa-solid fa-building"></i>
|
||||
<p>Brak klientów. Dodaj pierwszego klienta.</p>
|
||||
</td>
|
||||
@@ -121,6 +134,11 @@
|
||||
<label for="client-gads-id">Google Ads Customer ID</label>
|
||||
<input type="text" id="client-gads-id" name="google_ads_customer_id" class="form-control" placeholder="np. 123-456-7890 (opcjonalnie)" />
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="client-fbads-id">Facebook Ads Account ID</label>
|
||||
<input type="text" id="client-fbads-id" name="facebook_ads_account_id" class="form-control" placeholder="np. act_123456789012345 (opcjonalnie)" />
|
||||
<small class="text-muted">Mozesz podac act_... albo same cyfry</small>
|
||||
</div>
|
||||
<div class="settings-field">
|
||||
<label for="client-gmc-id">Merchant Account ID</label>
|
||||
<input type="text" id="client-gmc-id" name="google_merchant_account_id" class="form-control" placeholder="np. 123456789 (opcjonalnie)" />
|
||||
@@ -148,6 +166,7 @@ function openClientForm()
|
||||
$( '#client-name' ).val( '' );
|
||||
$( '#client-active' ).val( '1' );
|
||||
$( '#client-gads-id' ).val( '' );
|
||||
$( '#client-fbads-id' ).val( '' );
|
||||
$( '#client-gmc-id' ).val( '' );
|
||||
$( '#client-gads-start' ).val( '' );
|
||||
$( '#client-modal' ).fadeIn();
|
||||
@@ -166,6 +185,7 @@ function editClient( id )
|
||||
$( '#client-name' ).val( data.name );
|
||||
$( '#client-active' ).val( parseInt( data.active, 10 ) === 0 ? '0' : '1' );
|
||||
$( '#client-gads-id' ).val( data.google_ads_customer_id || '' );
|
||||
$( '#client-fbads-id' ).val( data.facebook_ads_account_id || '' );
|
||||
$( '#client-gmc-id' ).val( data.google_merchant_account_id || '' );
|
||||
$( '#client-gads-start' ).val( data.google_ads_start_date || '' );
|
||||
$( '#client-modal' ).fadeIn();
|
||||
@@ -245,7 +265,8 @@ function syncClient( id, pipeline, btn )
|
||||
var labels = {
|
||||
campaigns: 'kampanii',
|
||||
products: 'produktow',
|
||||
campaigns_product_alerts_merchant: 'walidacji Merchant'
|
||||
campaigns_product_alerts_merchant: 'walidacji Merchant',
|
||||
facebook_ads: 'Facebook Ads'
|
||||
};
|
||||
|
||||
$.post( '/clients/force_sync', { id: id, pipeline: pipeline }, function( response )
|
||||
@@ -259,9 +280,16 @@ function syncClient( id, pipeline, btn )
|
||||
{
|
||||
$btn.addClass( 'is-queued' );
|
||||
|
||||
var cron_hint = pipeline === 'facebook_ads'
|
||||
? ' Dane zostana pobrane przy najblizszym uruchomieniu /cron/cron_facebook_ads.'
|
||||
: ' Dane zostana pobrane przy najblizszym uruchomieniu CRON.';
|
||||
var refresh_hint = pipeline === 'facebook_ads'
|
||||
? ' Wymuszenie Facebook Ads nadpisuje dane dla okresu z config.php i pobiera tylko aktywne kampanie/zestawy/reklamy.'
|
||||
: '';
|
||||
|
||||
$.alert({
|
||||
title: 'Zakolejkowano',
|
||||
content: 'Synchronizacja ' + labels[ pipeline ] + ' zostala zakolejkowana. Dane zostana pobrane przy najblizszym uruchomieniu CRON.',
|
||||
content: 'Synchronizacja ' + labels[ pipeline ] + ' zostala zakolejkowana.' + cron_hint + refresh_hint,
|
||||
type: 'green',
|
||||
autoClose: 'ok|3000'
|
||||
});
|
||||
@@ -362,6 +390,7 @@ function loadSyncStatus()
|
||||
if ( info.campaigns ) html += renderSyncBar( 'K:', info.campaigns[0], info.campaigns[1] );
|
||||
if ( info.products ) html += renderSyncBar( 'P:', info.products[0], info.products[1] );
|
||||
if ( info.merchant ) html += renderSyncBar( 'M:', info.merchant[0], info.merchant[1] );
|
||||
if ( info.facebook_ads ) html += renderSyncBar( 'FB:', info.facebook_ads[0], info.facebook_ads[1] );
|
||||
html += '</div>';
|
||||
|
||||
$cell.html( html );
|
||||
|
||||
448
templates/facebook_ads/main_view.php
Normal file
448
templates/facebook_ads/main_view.php
Normal file
@@ -0,0 +1,448 @@
|
||||
<div class="campaigns-page">
|
||||
<div class="campaigns-header">
|
||||
<h2><i class="fa-brands fa-facebook"></i> Facebook Ads</h2>
|
||||
</div>
|
||||
|
||||
<div class="campaigns-filters">
|
||||
<div class="filter-group">
|
||||
<label for="fb_client_id"><i class="fa-solid fa-building"></i> Klient</label>
|
||||
<select id="fb_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" style="min-width: 280px; flex: 1;">
|
||||
<label for="fb_campaign_id"><i class="fa-solid fa-bullhorn"></i> Kampania</label>
|
||||
<select id="fb_campaign_id" class="form-control">
|
||||
<option value="">- wybierz kampanie -</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group" style="min-width: 280px; flex: 1;">
|
||||
<label for="fb_ad_set_id"><i class="fa-solid fa-layer-group"></i> Zestaw reklam</label>
|
||||
<select id="fb_ad_set_id" class="form-control">
|
||||
<option value="">- wybierz zestaw reklam -</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-group" style="min-width: 280px; flex: 1;">
|
||||
<label for="fb_ad_id"><i class="fa-solid fa-rectangle-ad"></i> Reklama</label>
|
||||
<select id="fb_ad_id" class="form-control">
|
||||
<option value="">- wybierz reklame -</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="campaigns-chart-wrap">
|
||||
<div id="fb_container"></div>
|
||||
</div>
|
||||
|
||||
<div class="campaigns-table-wrap">
|
||||
<table class="table" id="fb_history_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Data</th>
|
||||
<th>Spend</th>
|
||||
<th>Impressions</th>
|
||||
<th>Clicks</th>
|
||||
<th>CTR</th>
|
||||
<th>CPC</th>
|
||||
<th>Wartosc konwersji</th>
|
||||
<th>ROAS (30 dni)</th>
|
||||
<th>ROAS (all time)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var FB_FILTERS_KEY = 'facebook_ads_filters_v2';
|
||||
|
||||
function fb_save_filters_state()
|
||||
{
|
||||
var state = {
|
||||
client_id: $( '#fb_client_id' ).val() || '',
|
||||
campaign_id: $( '#fb_campaign_id' ).val() || '',
|
||||
ad_set_id: $( '#fb_ad_set_id' ).val() || '',
|
||||
ad_id: $( '#fb_ad_id' ).val() || ''
|
||||
};
|
||||
|
||||
localStorage.setItem( FB_FILTERS_KEY, JSON.stringify( state ) );
|
||||
}
|
||||
|
||||
function fb_load_filters_state()
|
||||
{
|
||||
try
|
||||
{
|
||||
var raw = localStorage.getItem( FB_FILTERS_KEY );
|
||||
var parsed = JSON.parse( raw || '{}' );
|
||||
|
||||
return {
|
||||
client_id: parsed.client_id || '',
|
||||
campaign_id: parsed.campaign_id || '',
|
||||
ad_set_id: parsed.ad_set_id || '',
|
||||
ad_id: parsed.ad_id || ''
|
||||
};
|
||||
}
|
||||
catch ( e )
|
||||
{
|
||||
return { client_id: '', campaign_id: '', ad_set_id: '', ad_id: '' };
|
||||
}
|
||||
}
|
||||
|
||||
function fb_get_filters()
|
||||
{
|
||||
var campaign_id = $( '#fb_campaign_id' ).val() || '';
|
||||
var ad_set_id = $( '#fb_ad_set_id' ).val() || '';
|
||||
var ad_id = $( '#fb_ad_id' ).val() || '';
|
||||
|
||||
var level = '';
|
||||
var entity_id = '';
|
||||
|
||||
if ( ad_id )
|
||||
{
|
||||
level = 'ad';
|
||||
entity_id = ad_id;
|
||||
}
|
||||
else if ( ad_set_id )
|
||||
{
|
||||
level = 'adset';
|
||||
entity_id = ad_set_id;
|
||||
}
|
||||
else if ( campaign_id )
|
||||
{
|
||||
level = 'campaign';
|
||||
entity_id = campaign_id;
|
||||
}
|
||||
|
||||
return {
|
||||
client_id: $( '#fb_client_id' ).val() || '',
|
||||
campaign_id: campaign_id,
|
||||
ad_set_id: ad_set_id,
|
||||
ad_id: ad_id,
|
||||
level: level,
|
||||
entity_id: entity_id
|
||||
};
|
||||
}
|
||||
|
||||
function fb_reload_entities( level, options, done )
|
||||
{
|
||||
var filters = fb_get_filters();
|
||||
var opts = options || {};
|
||||
var selected_id = opts.selected_id || '';
|
||||
var $entity = null;
|
||||
var default_label = '';
|
||||
|
||||
if ( level === 'campaign' )
|
||||
{
|
||||
$entity = $( '#fb_campaign_id' );
|
||||
default_label = '- wybierz kampanie -';
|
||||
}
|
||||
else if ( level === 'adset' )
|
||||
{
|
||||
$entity = $( '#fb_ad_set_id' );
|
||||
default_label = '- wybierz zestaw reklam -';
|
||||
}
|
||||
else
|
||||
{
|
||||
$entity = $( '#fb_ad_id' );
|
||||
default_label = '- wybierz reklame -';
|
||||
}
|
||||
|
||||
$entity.empty().append( '<option value="">' + default_label + '</option>' );
|
||||
|
||||
if ( !filters.client_id )
|
||||
{
|
||||
if ( typeof done === 'function' )
|
||||
{
|
||||
done();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = { client_id: filters.client_id, level: level };
|
||||
if ( level === 'adset' || level === 'ad' )
|
||||
{
|
||||
payload.campaign_id = filters.campaign_id;
|
||||
}
|
||||
if ( level === 'ad' )
|
||||
{
|
||||
payload.ad_set_id = filters.ad_set_id;
|
||||
}
|
||||
|
||||
$.ajax( {
|
||||
url: '/facebook_ads/get_entities',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
data: payload,
|
||||
success: function( resp )
|
||||
{
|
||||
var rows = resp.entities || [];
|
||||
|
||||
rows.forEach( function( row )
|
||||
{
|
||||
var id = row.id;
|
||||
var name = row.entity_name || ( '#' + ( row.external_id || id ) );
|
||||
$entity.append( new Option( name, id ) );
|
||||
} );
|
||||
|
||||
if ( selected_id && $entity.find( 'option[value="' + selected_id + '"]' ).length )
|
||||
{
|
||||
$entity.val( selected_id );
|
||||
}
|
||||
},
|
||||
complete: function()
|
||||
{
|
||||
if ( typeof done === 'function' )
|
||||
{
|
||||
done();
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
function fb_clear_chart_and_table()
|
||||
{
|
||||
if ( $.fn.DataTable.isDataTable( '#fb_history_table' ) )
|
||||
{
|
||||
$( '#fb_history_table' ).DataTable().destroy();
|
||||
$( '#fb_history_table tbody' ).empty();
|
||||
}
|
||||
|
||||
Highcharts.chart( 'fb_container', {
|
||||
title: { text: '' },
|
||||
series: []
|
||||
} );
|
||||
}
|
||||
|
||||
function fb_reload_table()
|
||||
{
|
||||
var filters = fb_get_filters();
|
||||
var is_campaign = filters.level === 'campaign';
|
||||
|
||||
if ( $.fn.DataTable.isDataTable( '#fb_history_table' ) )
|
||||
{
|
||||
$( '#fb_history_table' ).DataTable().destroy();
|
||||
$( '#fb_history_table tbody' ).empty();
|
||||
}
|
||||
|
||||
if ( !filters.entity_id )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var columns = [
|
||||
{ width: '130px', orderable: false, className: 'nowrap' },
|
||||
{ width: '120px', orderable: false, className: 'dt-type-numeric' },
|
||||
{ width: '120px', orderable: false, className: 'dt-type-numeric' },
|
||||
{ width: '120px', orderable: false, className: 'dt-type-numeric' },
|
||||
{ width: '120px', orderable: false, className: 'dt-type-numeric' },
|
||||
{ width: '120px', orderable: false, className: 'dt-type-numeric' },
|
||||
{ width: '140px', orderable: false, className: 'dt-type-numeric' },
|
||||
{ width: '120px', orderable: false, className: 'dt-type-numeric' },
|
||||
{ width: '140px', orderable: false, className: 'dt-type-numeric', visible: is_campaign }
|
||||
];
|
||||
|
||||
new DataTable( '#fb_history_table', {
|
||||
ajax: {
|
||||
type: 'POST',
|
||||
url: '/facebook_ads/get_history_data_table',
|
||||
data: function( d )
|
||||
{
|
||||
d.level = filters.level;
|
||||
d.entity_id = filters.entity_id;
|
||||
}
|
||||
},
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
searching: false,
|
||||
lengthChange: false,
|
||||
pageLength: 15,
|
||||
order: [],
|
||||
columns: columns,
|
||||
language: {
|
||||
processing: 'Ladowanie...',
|
||||
emptyTable: 'Brak danych do wyswietlenia',
|
||||
info: 'Wpisy _START_ - _END_ z _TOTAL_',
|
||||
infoEmpty: '',
|
||||
paginate: {
|
||||
first: 'Pierwsza',
|
||||
last: 'Ostatnia',
|
||||
next: 'Dalej',
|
||||
previous: 'Wstecz'
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
function fb_reload_chart()
|
||||
{
|
||||
var filters = fb_get_filters();
|
||||
|
||||
Highcharts.chart( 'fb_container', {
|
||||
title: { text: '' },
|
||||
series: []
|
||||
} );
|
||||
|
||||
if ( !filters.entity_id )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax( {
|
||||
url: '/facebook_ads/get_history_data_chart',
|
||||
method: 'POST',
|
||||
data: { level: filters.level, entity_id: filters.entity_id },
|
||||
success: function( response )
|
||||
{
|
||||
var parsed = JSON.parse( response || '{}' );
|
||||
var dates = parsed.dates || [];
|
||||
var chart_data = parsed.chart_data || [];
|
||||
|
||||
Highcharts.chart( 'fb_container', {
|
||||
chart: {
|
||||
style: { fontFamily: '"Roboto", sans-serif' },
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
title: { text: '' },
|
||||
xAxis: { categories: dates },
|
||||
yAxis: { title: { text: '' } },
|
||||
legend: {
|
||||
layout: 'horizontal',
|
||||
align: 'center',
|
||||
verticalAlign: 'bottom'
|
||||
},
|
||||
colors: [ '#1877F2', '#57B951', '#FF8C00', '#8B5CF6', '#CC0000', '#1F9D8B', '#A16207', '#E91E63' ],
|
||||
series: chart_data,
|
||||
credits: { enabled: false }
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
function fb_reload_data()
|
||||
{
|
||||
fb_save_filters_state();
|
||||
|
||||
var filters = fb_get_filters();
|
||||
if ( !filters.entity_id )
|
||||
{
|
||||
fb_clear_chart_and_table();
|
||||
return;
|
||||
}
|
||||
|
||||
fb_reload_table();
|
||||
fb_reload_chart();
|
||||
}
|
||||
|
||||
function fb_reload_campaigns( selected_id, done )
|
||||
{
|
||||
fb_reload_entities( 'campaign', { selected_id: selected_id }, done );
|
||||
}
|
||||
|
||||
function fb_reload_ad_sets( selected_id, done )
|
||||
{
|
||||
fb_reload_entities( 'adset', { selected_id: selected_id }, done );
|
||||
}
|
||||
|
||||
function fb_reload_ads( selected_id, done )
|
||||
{
|
||||
fb_reload_entities( 'ad', { selected_id: selected_id }, done );
|
||||
}
|
||||
|
||||
$( function()
|
||||
{
|
||||
var saved = fb_load_filters_state();
|
||||
|
||||
if ( saved.client_id )
|
||||
{
|
||||
$( '#fb_client_id' ).val( saved.client_id );
|
||||
}
|
||||
|
||||
$( 'body' ).on( 'change', '#fb_client_id', function()
|
||||
{
|
||||
$( '#fb_campaign_id' ).empty().append( '<option value="">- wybierz kampanie -</option>' );
|
||||
$( '#fb_ad_set_id' ).empty().append( '<option value="">- wybierz zestaw reklam -</option>' );
|
||||
$( '#fb_ad_id' ).empty().append( '<option value="">- wybierz reklame -</option>' );
|
||||
|
||||
fb_reload_campaigns( '', function()
|
||||
{
|
||||
fb_reload_data();
|
||||
} );
|
||||
} );
|
||||
|
||||
$( 'body' ).on( 'change', '#fb_campaign_id', function()
|
||||
{
|
||||
$( '#fb_ad_set_id' ).empty().append( '<option value="">- wybierz zestaw reklam -</option>' );
|
||||
$( '#fb_ad_id' ).empty().append( '<option value="">- wybierz reklame -</option>' );
|
||||
|
||||
if ( !$( '#fb_campaign_id' ).val() )
|
||||
{
|
||||
fb_reload_data();
|
||||
return;
|
||||
}
|
||||
|
||||
fb_reload_ad_sets( '', function()
|
||||
{
|
||||
fb_reload_data();
|
||||
} );
|
||||
} );
|
||||
|
||||
$( 'body' ).on( 'change', '#fb_ad_set_id', function()
|
||||
{
|
||||
$( '#fb_ad_id' ).empty().append( '<option value="">- wybierz reklame -</option>' );
|
||||
|
||||
if ( !$( '#fb_ad_set_id' ).val() )
|
||||
{
|
||||
fb_reload_data();
|
||||
return;
|
||||
}
|
||||
|
||||
fb_reload_ads( '', function()
|
||||
{
|
||||
fb_reload_data();
|
||||
} );
|
||||
} );
|
||||
|
||||
$( 'body' ).on( 'change', '#fb_ad_id', function()
|
||||
{
|
||||
fb_reload_data();
|
||||
} );
|
||||
|
||||
if ( $( '#fb_client_id' ).val() )
|
||||
{
|
||||
fb_reload_campaigns( saved.campaign_id, function()
|
||||
{
|
||||
if ( !$( '#fb_campaign_id' ).val() )
|
||||
{
|
||||
fb_reload_data();
|
||||
return;
|
||||
}
|
||||
|
||||
fb_reload_ad_sets( saved.ad_set_id, function()
|
||||
{
|
||||
if ( !$( '#fb_ad_set_id' ).val() )
|
||||
{
|
||||
fb_reload_data();
|
||||
return;
|
||||
}
|
||||
|
||||
fb_reload_ads( saved.ad_id, function()
|
||||
{
|
||||
fb_reload_data();
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
}
|
||||
else
|
||||
{
|
||||
fb_reload_data();
|
||||
}
|
||||
} );
|
||||
</script>
|
||||
@@ -31,10 +31,6 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group filter-group-roas">
|
||||
<label for="bestseller_min_roas"><i class="fa-solid fa-star"></i> Bestseller min ROAS</label>
|
||||
<input type="text" id="bestseller_min_roas" name="bestseller_min_roas" class="form-control" placeholder="np. 500" value="" />
|
||||
</div>
|
||||
<div class="filter-group filter-group-columns">
|
||||
<label><i class="fa-solid fa-table-columns"></i> Kolumny</label>
|
||||
<details class="products-columns-control">
|
||||
@@ -521,25 +517,6 @@ $( function()
|
||||
return false;
|
||||
}
|
||||
|
||||
function load_client_bestseller_min_roas( client_id )
|
||||
{
|
||||
if ( !client_id )
|
||||
{
|
||||
$( '#bestseller_min_roas' ).val( '' );
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/products/get_client_bestseller_min_roas/',
|
||||
type: 'POST',
|
||||
data: { client_id: client_id },
|
||||
success: function( response ) {
|
||||
var data = JSON.parse( response );
|
||||
$( '#bestseller_min_roas' ).val( data.status == 'ok' ? data.min_roas : '' );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function load_products_campaigns( client_id, selected_campaign_id )
|
||||
{
|
||||
var $campaign = $( '#products_campaign_id' );
|
||||
@@ -1084,7 +1061,6 @@ $( function()
|
||||
localStorage.removeItem( 'products_ad_group_id' );
|
||||
update_delete_ad_group_button_state();
|
||||
|
||||
load_client_bestseller_min_roas( client_id );
|
||||
load_products_campaigns( client_id, '' ).done( function() {
|
||||
load_products_ad_groups( '', '' ).done( function() {
|
||||
update_delete_ad_group_button_state();
|
||||
@@ -1189,7 +1165,6 @@ $( function()
|
||||
$( '#client_id' ).val( savedClient );
|
||||
}
|
||||
|
||||
load_client_bestseller_min_roas( $( '#client_id' ).val() || '' );
|
||||
load_products_campaigns( $( '#client_id' ).val() || '', savedCampaign ).done( function() {
|
||||
var selected_campaign_id = $( '#products_campaign_id' ).val() || '';
|
||||
load_products_ad_groups( selected_campaign_id, savedAdGroup ).done( function() {
|
||||
@@ -1631,25 +1606,6 @@ $( function()
|
||||
});
|
||||
});
|
||||
|
||||
// Zapis min ROAS klienta (bestseller)
|
||||
$( 'body' ).on( 'blur', '#bestseller_min_roas', function()
|
||||
{
|
||||
var min_roas = $( this ).val();
|
||||
var client_id = $( '#client_id' ).val();
|
||||
|
||||
$.ajax({
|
||||
url: '/products/save_client_bestseller_min_roas/',
|
||||
type: 'POST',
|
||||
data: { client_id: client_id, min_roas: min_roas },
|
||||
success: function( response ) {
|
||||
data = JSON.parse( response );
|
||||
if ( data.status == 'ok' ) {
|
||||
$.alert({ title: 'Zapisano', content: 'Minimalny ROAS bestsellerów został zapisany.', type: 'green', autoClose: 'ok|2000', buttons: { ok: function() {} } });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Checkbox: zaznacz/odznacz wszystkie
|
||||
function updateSelectedCount() {
|
||||
var count = $( '.product-checkbox:checked' ).length;
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
$module = $this -> current_module;
|
||||
$google_ads_modules = [ 'campaigns', 'campaign_terms', 'products', 'campaign_alerts', 'clients', 'xml_files' ];
|
||||
$is_google_ads_module = in_array( $module, $google_ads_modules, true );
|
||||
$facebook_ads_modules = [ 'facebook_ads' ];
|
||||
$is_facebook_ads_module = in_array( $module, $facebook_ads_modules, true );
|
||||
?>
|
||||
<!-- Sidebar -->
|
||||
<aside class="sidebar" id="sidebar">
|
||||
@@ -101,6 +103,20 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-group <?= $is_facebook_ads_module ? 'active' : '' ?>">
|
||||
<div class="nav-group-label">
|
||||
<i class="fa-brands fa-facebook"></i>
|
||||
<span>Facebook ADS</span>
|
||||
</div>
|
||||
<ul class="nav-submenu">
|
||||
<li class="<?= $module === 'facebook_ads' ? 'active' : '' ?>">
|
||||
<a href="/facebook_ads">
|
||||
<i class="fa-solid fa-chart-line"></i>
|
||||
<span>Kampanie i reklamy</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="<?= $module === 'allegro' ? 'active' : '' ?>">
|
||||
<a href="/allegro">
|
||||
<i class="fa-solid fa-file-import"></i>
|
||||
@@ -147,6 +163,7 @@
|
||||
'campaign_alerts' => 'Alerty',
|
||||
'clients' => 'Klienci',
|
||||
'xml_files' => 'Pliki XML',
|
||||
'facebook_ads' => 'Facebook Ads',
|
||||
'allegro' => 'Allegro import',
|
||||
'users' => 'Ustawienia',
|
||||
];
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<div class="cron-status-overview">
|
||||
<div><strong>Ostatnie wywołanie:</strong> <span data-cron-overall-last-invoked><?= htmlspecialchars( (string) ( $cron_data['overall_last_invoked_at'] ?? 'Brak danych' ) ); ?></span></div>
|
||||
<div><strong>Klienci z Google Ads ID:</strong> <span data-cron-clients-total><?= (int) ( $cron_data['clients_total'] ?? 0 ); ?></span></div>
|
||||
<div><strong>Klienci z Facebook Ads ID:</strong> <span data-cron-fb-clients-total><?= (int) ( $cron_data['facebook_clients_total'] ?? 0 ); ?></span></div>
|
||||
</div>
|
||||
|
||||
<div class="cron-progress-list">
|
||||
@@ -120,10 +121,24 @@
|
||||
</div>
|
||||
<?php
|
||||
$last_error = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
|
||||
$last_error_at_raw = \services\GoogleAdsApi::get_setting( 'google_ads_last_error_at' );
|
||||
$last_error_at = '';
|
||||
if ( $last_error_at_raw )
|
||||
{
|
||||
$last_error_at_ts = strtotime( (string) $last_error_at_raw );
|
||||
if ( $last_error_at_ts )
|
||||
{
|
||||
$last_error_at = date( 'Y-m-d H:i:s', $last_error_at_ts );
|
||||
}
|
||||
}
|
||||
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 style="min-width:0;">
|
||||
<div><strong>Ostatni blad API:</strong></div>
|
||||
<div><strong>Data wystapienia:</strong> <?= htmlspecialchars( $last_error_at !== '' ? $last_error_at : 'Brak danych' ); ?></div>
|
||||
<div style="margin-top:6px;white-space:pre-wrap;word-break:break-word;"><?= htmlspecialchars( $last_error ); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form method="POST" id="google-ads-settings" action="/settings/save_google_ads">
|
||||
@@ -401,6 +416,7 @@
|
||||
|
||||
set_text( '[data-cron-overall-last-invoked]', data.overall_last_invoked_at || 'Brak danych' );
|
||||
set_text( '[data-cron-clients-total]', data.clients_total || 0 );
|
||||
set_text( '[data-cron-fb-clients-total]', data.facebook_clients_total || 0 );
|
||||
render_progress( data.progress );
|
||||
render_urls( data.urls );
|
||||
}
|
||||
@@ -484,3 +500,4 @@
|
||||
})();
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<div class="clients-page xml-files-page">
|
||||
<div class="clients-header">
|
||||
<h2><i class="fa-solid fa-file-code"></i> Pliki XML</h2>
|
||||
</div>
|
||||
|
||||
<div class="clients-table-wrap">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 60px;">#ID</th>
|
||||
<th>Klient</th>
|
||||
<th>Google Ads ID</th>
|
||||
<th>Link do custom feed</th>
|
||||
<th style="width: 180px;">Ostatnia modyfikacja</th>
|
||||
<th style="width: 220px; text-align: center;">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ( $this -> rows ): ?>
|
||||
<?php foreach ( $this -> rows as $row ): ?>
|
||||
<tr>
|
||||
<td class="client-id"><?= (int) ( $row['client_id'] ?? 0 ); ?></td>
|
||||
<td class="client-name"><?= htmlspecialchars( (string) ( $row['client_name'] ?? '' ) ); ?></td>
|
||||
<td>
|
||||
<?php if ( !empty( $row['google_ads_customer_id'] ) ): ?>
|
||||
<span class="badge-id"><?= htmlspecialchars( (string) $row['google_ads_customer_id'] ); ?></span>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">- brak -</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<a href="<?= htmlspecialchars( (string) ( $row['xml_url'] ?? '' ) ); ?>" target="_blank" rel="noopener">
|
||||
<?= htmlspecialchars( (string) ( $row['xml_url'] ?? '' ) ); ?>
|
||||
</a>
|
||||
<?php if ( empty( $row['xml_exists'] ) ): ?>
|
||||
<div class="text-muted">Plik nie zostal jeszcze wygenerowany.</div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ( !empty( $row['xml_last_modified'] ) ): ?>
|
||||
<?= htmlspecialchars( (string) $row['xml_last_modified'] ); ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">Brak</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="actions-cell" style="justify-content: center;">
|
||||
<form method="POST" action="/xml_files/regenerate" style="margin: 0;">
|
||||
<input type="hidden" name="client_id" value="<?= (int) ( $row['client_id'] ?? 0 ); ?>">
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="fa-solid fa-rotate-right mr5"></i> Wymus generowanie
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="6" class="empty-state">
|
||||
<i class="fa-solid fa-file-code"></i>
|
||||
<p>Brak klientow do wyswietlenia.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
4482
tmp/meta_active_last30d.json
Normal file
4482
tmp/meta_active_last30d.json
Normal file
File diff suppressed because it is too large
Load Diff
257
tools/meta-ads-active-insights.php
Normal file
257
tools/meta-ads-active-insights.php
Normal file
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple CLI script to fetch active Meta Ads insights for:
|
||||
* campaign, adset, ad levels for the last N days (default 30).
|
||||
*
|
||||
* Usage:
|
||||
* php tools/meta-ads-active-insights.php --token=TOKEN --account=act_123456789 --days=30
|
||||
* php tools/meta-ads-active-insights.php --token=TOKEN --account=123456789 --output=tmp/meta_insights.json
|
||||
*
|
||||
* You can also use env vars:
|
||||
* META_ACCESS_TOKEN
|
||||
* META_AD_ACCOUNT_ID
|
||||
*/
|
||||
|
||||
if ( PHP_SAPI !== 'cli' )
|
||||
{
|
||||
fwrite( STDERR, "This script must be run from CLI.\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
$options = getopt( '', [
|
||||
'token::',
|
||||
'account::',
|
||||
'days::',
|
||||
'api-version::',
|
||||
'output::',
|
||||
'help::'
|
||||
] );
|
||||
|
||||
if ( isset( $options['help'] ) )
|
||||
{
|
||||
echo "Usage:\n";
|
||||
echo " php tools/meta-ads-active-insights.php --token=TOKEN --account=act_123 --days=30\n";
|
||||
echo " php tools/meta-ads-active-insights.php --token=TOKEN --account=123 --output=tmp/meta.json\n";
|
||||
echo "\n";
|
||||
echo "Options:\n";
|
||||
echo " --token Meta access token (or META_ACCESS_TOKEN env)\n";
|
||||
echo " --account Ad account id, with or without act_ prefix (or META_AD_ACCOUNT_ID env)\n";
|
||||
echo " --days Number of days back, default 30\n";
|
||||
echo " --api-version Graph API version, default v25.0\n";
|
||||
echo " --output Optional output JSON file path\n";
|
||||
exit( 0 );
|
||||
}
|
||||
|
||||
$token = trim( (string) ( $options['token'] ?? getenv( 'META_ACCESS_TOKEN' ) ?? '' ) );
|
||||
$account_id = trim( (string) ( $options['account'] ?? getenv( 'META_AD_ACCOUNT_ID' ) ?? '' ) );
|
||||
$days = (int) ( $options['days'] ?? 30 );
|
||||
$api_version = trim( (string) ( $options['api-version'] ?? 'v25.0' ) );
|
||||
$output = trim( (string) ( $options['output'] ?? '' ) );
|
||||
|
||||
if ( $token === '' )
|
||||
{
|
||||
fwrite( STDERR, "Missing token. Use --token or META_ACCESS_TOKEN.\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
if ( $account_id === '' )
|
||||
{
|
||||
fwrite( STDERR, "Missing ad account id. Use --account or META_AD_ACCOUNT_ID.\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
if ( strpos( $account_id, 'act_' ) !== 0 )
|
||||
{
|
||||
$account_id = 'act_' . preg_replace( '/\D+/', '', $account_id );
|
||||
}
|
||||
|
||||
if ( $account_id === 'act_' )
|
||||
{
|
||||
fwrite( STDERR, "Invalid ad account id.\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
if ( $days < 1 )
|
||||
{
|
||||
$days = 1;
|
||||
}
|
||||
|
||||
$since = date( 'Y-m-d', strtotime( '-' . ( $days - 1 ) . ' days' ) );
|
||||
$until = date( 'Y-m-d' );
|
||||
$base_url = 'https://graph.facebook.com/' . rawurlencode( $api_version ) . '/' . rawurlencode( $account_id ) . '/insights';
|
||||
|
||||
$levels = [
|
||||
'campaign' => [
|
||||
'fields' => 'account_id,campaign_id,campaign_name,spend,impressions,clicks,ctr,cpc,action_values,purchase_roas,date_start,date_stop',
|
||||
'filtering_field' => 'campaign.effective_status'
|
||||
],
|
||||
'adset' => [
|
||||
'fields' => 'account_id,campaign_id,campaign_name,adset_id,adset_name,spend,impressions,clicks,ctr,cpc,action_values,purchase_roas,date_start,date_stop',
|
||||
'filtering_field' => 'adset.effective_status'
|
||||
],
|
||||
'ad' => [
|
||||
'fields' => 'account_id,campaign_id,campaign_name,adset_id,adset_name,ad_id,ad_name,spend,impressions,clicks,ctr,cpc,action_values,purchase_roas,date_start,date_stop',
|
||||
'filtering_field' => 'ad.effective_status'
|
||||
]
|
||||
];
|
||||
|
||||
$result = [
|
||||
'meta' => [
|
||||
'account_id' => $account_id,
|
||||
'api_version' => $api_version,
|
||||
'days' => $days,
|
||||
'since' => $since,
|
||||
'until' => $until,
|
||||
'generated_at' => date( 'c' )
|
||||
],
|
||||
'campaign' => [],
|
||||
'adset' => [],
|
||||
'ad' => [],
|
||||
'summary' => []
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
foreach ( $levels as $level => $cfg )
|
||||
{
|
||||
fwrite( STDERR, "Fetching level: {$level}\n" );
|
||||
|
||||
$params = [
|
||||
'access_token' => $token,
|
||||
'level' => $level,
|
||||
'fields' => $cfg['fields'],
|
||||
'time_increment' => 1,
|
||||
'time_range' => json_encode( [ 'since' => $since, 'until' => $until ] ),
|
||||
'filtering' => json_encode( [
|
||||
[
|
||||
'field' => $cfg['filtering_field'],
|
||||
'operator' => 'IN',
|
||||
'value' => [ 'ACTIVE' ]
|
||||
]
|
||||
] ),
|
||||
'limit' => 500
|
||||
];
|
||||
|
||||
$rows = fetch_all_pages( $base_url, $params );
|
||||
$result[ $level ] = $rows;
|
||||
$result['summary'][ $level . '_rows' ] = count( $rows );
|
||||
}
|
||||
}
|
||||
catch ( Exception $e )
|
||||
{
|
||||
fwrite( STDERR, "Error: " . $e -> getMessage() . "\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
$json = json_encode( $result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT );
|
||||
|
||||
if ( $json === false )
|
||||
{
|
||||
fwrite( STDERR, "Failed to encode JSON output.\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
if ( $output !== '' )
|
||||
{
|
||||
$output_dir = dirname( $output );
|
||||
if ( $output_dir !== '' && $output_dir !== '.' && !is_dir( $output_dir ) )
|
||||
{
|
||||
if ( !mkdir( $output_dir, 0775, true ) && !is_dir( $output_dir ) )
|
||||
{
|
||||
fwrite( STDERR, "Failed to create output directory: {$output_dir}\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
}
|
||||
|
||||
if ( file_put_contents( $output, $json ) === false )
|
||||
{
|
||||
fwrite( STDERR, "Failed to write output file: {$output}\n" );
|
||||
exit( 1 );
|
||||
}
|
||||
|
||||
fwrite( STDERR, "Saved: {$output}\n" );
|
||||
}
|
||||
else
|
||||
{
|
||||
echo $json . PHP_EOL;
|
||||
}
|
||||
|
||||
exit( 0 );
|
||||
|
||||
function fetch_all_pages( $url, $params = null )
|
||||
{
|
||||
$all_rows = [];
|
||||
$next_url = $url;
|
||||
$next_params = $params;
|
||||
|
||||
while ( $next_url )
|
||||
{
|
||||
$payload = request_json( $next_url, $next_params );
|
||||
|
||||
if ( isset( $payload['data'] ) && is_array( $payload['data'] ) )
|
||||
{
|
||||
foreach ( $payload['data'] as $row )
|
||||
{
|
||||
$all_rows[] = $row;
|
||||
}
|
||||
}
|
||||
|
||||
$next_url = '';
|
||||
$next_params = null;
|
||||
|
||||
if ( isset( $payload['paging']['next'] ) && is_string( $payload['paging']['next'] ) )
|
||||
{
|
||||
$next_url = $payload['paging']['next'];
|
||||
}
|
||||
}
|
||||
|
||||
return $all_rows;
|
||||
}
|
||||
|
||||
function request_json( $url, $params = null )
|
||||
{
|
||||
if ( is_array( $params ) )
|
||||
{
|
||||
$query = http_build_query( $params );
|
||||
$url .= ( strpos( $url, '?' ) === false ? '?' : '&' ) . $query;
|
||||
}
|
||||
|
||||
$ch = curl_init( $url );
|
||||
curl_setopt_array( $ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 20,
|
||||
CURLOPT_TIMEOUT => 120
|
||||
] );
|
||||
|
||||
$response = curl_exec( $ch );
|
||||
$curl_error = curl_error( $ch );
|
||||
$http_code = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||
curl_close( $ch );
|
||||
|
||||
if ( $response === false )
|
||||
{
|
||||
throw new Exception( 'cURL error: ' . $curl_error );
|
||||
}
|
||||
|
||||
$decoded = json_decode( $response, true );
|
||||
if ( !is_array( $decoded ) )
|
||||
{
|
||||
throw new Exception( 'Invalid JSON response. HTTP ' . $http_code . '. Body: ' . substr( (string) $response, 0, 1000 ) );
|
||||
}
|
||||
|
||||
if ( isset( $decoded['error'] ) )
|
||||
{
|
||||
$message = (string) ( $decoded['error']['message'] ?? 'Unknown API error' );
|
||||
$code = (string) ( $decoded['error']['code'] ?? '' );
|
||||
$subcode = (string) ( $decoded['error']['error_subcode'] ?? '' );
|
||||
throw new Exception( 'Meta API error: ' . $message . ' (code: ' . $code . ', subcode: ' . $subcode . ')' );
|
||||
}
|
||||
|
||||
if ( $http_code >= 400 )
|
||||
{
|
||||
throw new Exception( 'HTTP error ' . $http_code . '. Body: ' . substr( (string) $response, 0, 1000 ) );
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
Reference in New Issue
Block a user