feat: add campaign alerts feature with alerts management and UI integration
- Introduced a new `CampaignAlerts` class for handling alerts logic. - Added database migration for `campaign_alerts` table creation. - Implemented methods for fetching, marking, and deleting alerts in the `CampaignAlerts` factory class. - Created a new view for displaying campaign alerts with filtering options. - Updated the main client view to include a badge for the number of alerts. - Enhanced sync functionality to support campaigns and products separately. - Adjusted styles for alert badges in the UI.
This commit is contained in:
@@ -15,7 +15,9 @@
|
||||
"Bash(git push:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(php:*)"
|
||||
"Bash(php:*)",
|
||||
"WebFetch(domain:adspro.projectpro.pl)",
|
||||
"mcp__ide__getDiagnostics"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@ class Tpl
|
||||
$this -> vars[ $name ] = $value;
|
||||
}
|
||||
|
||||
public function __isset( $name )
|
||||
{
|
||||
return isset( $this -> vars[ $name ] );
|
||||
}
|
||||
|
||||
public function __get( $name )
|
||||
{
|
||||
return $this -> vars[ $name ];
|
||||
|
||||
34
autoload/controls/class.CampaignAlerts.php
Normal file
34
autoload/controls/class.CampaignAlerts.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
namespace controls;
|
||||
|
||||
class CampaignAlerts
|
||||
{
|
||||
static public function main_view()
|
||||
{
|
||||
$client_id = (int) \S::get( 'client_id' );
|
||||
$page = max( 1, (int) \S::get( 'page' ) );
|
||||
$per_page = 15;
|
||||
$offset = ( $page - 1 ) * $per_page;
|
||||
|
||||
\factory\CampaignAlerts::mark_all_seen();
|
||||
\factory\CampaignAlerts::delete_old_alerts( 30 );
|
||||
|
||||
$total = \factory\CampaignAlerts::get_alerts_count( $client_id );
|
||||
$total_pages = max( 1, (int) ceil( $total / $per_page ) );
|
||||
|
||||
if ( $page > $total_pages )
|
||||
{
|
||||
$page = $total_pages;
|
||||
$offset = ( $page - 1 ) * $per_page;
|
||||
}
|
||||
|
||||
return \Tpl::view( 'campaign_alerts/main_view', [
|
||||
'clients' => \factory\CampaignAlerts::get_clients(),
|
||||
'alerts' => \factory\CampaignAlerts::get_alerts( $client_id, $per_page, $offset ),
|
||||
'selected_client_id' => $client_id,
|
||||
'page' => $page,
|
||||
'total_pages' => $total_pages,
|
||||
'total' => $total
|
||||
] );
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,8 @@ class Clients
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$id = (int) \S::get( 'id' );
|
||||
$id = (int) \S::get( 'id' );
|
||||
$pipeline = \S::get( 'pipeline' );
|
||||
|
||||
if ( !$id )
|
||||
{
|
||||
@@ -116,9 +117,16 @@ class Clients
|
||||
exit;
|
||||
}
|
||||
|
||||
$mdb -> delete( 'cron_sync_status', [ 'client_id' => $id ] );
|
||||
$where = [ 'client_id' => $id ];
|
||||
|
||||
echo json_encode( [ 'success' => true ] );
|
||||
if ( in_array( $pipeline, [ 'campaigns', 'products' ] ) )
|
||||
{
|
||||
$where['pipeline'] = $pipeline;
|
||||
}
|
||||
|
||||
$mdb -> delete( 'cron_sync_status', $where );
|
||||
|
||||
echo json_encode( [ 'success' => true, 'pipeline' => $pipeline ?: 'all' ] );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1955,6 +1955,7 @@ class Cron
|
||||
'search_terms_synced' => (int) ( $sync['search_terms_synced'] ?? 0 ),
|
||||
'keywords_synced' => (int) ( $sync['keywords_synced'] ?? 0 ),
|
||||
'negative_keywords_synced' => (int) ( $sync['negative_keywords_synced'] ?? 0 ),
|
||||
'alerts_synced' => (int) ( $sync['alerts_synced'] ?? 0 ),
|
||||
'errors' => $sync['errors']
|
||||
] );
|
||||
exit;
|
||||
@@ -2049,6 +2050,7 @@ class Cron
|
||||
$search_terms_synced_total = 0;
|
||||
$keywords_synced_total = 0;
|
||||
$negative_keywords_synced_total = 0;
|
||||
$alerts_synced_total = 0;
|
||||
|
||||
foreach ( $dates_batch as $active_date )
|
||||
{
|
||||
@@ -2060,6 +2062,7 @@ class Cron
|
||||
$search_terms_synced_total += (int) ( $sync['search_terms_synced'] ?? 0 );
|
||||
$keywords_synced_total += (int) ( $sync['keywords_synced'] ?? 0 );
|
||||
$negative_keywords_synced_total += (int) ( $sync['negative_keywords_synced'] ?? 0 );
|
||||
$alerts_synced_total += (int) ( $sync['alerts_synced'] ?? 0 );
|
||||
|
||||
$error_msg = null;
|
||||
if ( !empty( $sync['errors'] ) )
|
||||
@@ -2102,6 +2105,7 @@ class Cron
|
||||
'search_terms_synced' => $search_terms_synced_total,
|
||||
'keywords_synced' => $keywords_synced_total,
|
||||
'negative_keywords_synced' => $negative_keywords_synced_total,
|
||||
'alerts_synced' => $alerts_synced_total,
|
||||
'total_clients' => count( $client_ids ),
|
||||
'errors' => $errors
|
||||
] );
|
||||
@@ -2131,6 +2135,7 @@ class Cron
|
||||
'search_terms_synced' => 0,
|
||||
'keywords_synced' => 0,
|
||||
'negative_keywords_synced' => 0,
|
||||
'alerts_synced' => 0,
|
||||
'errors' => $errors
|
||||
];
|
||||
}
|
||||
@@ -2152,6 +2157,7 @@ class Cron
|
||||
'search_terms_synced' => 0,
|
||||
'keywords_synced' => 0,
|
||||
'negative_keywords_synced' => 0,
|
||||
'alerts_synced' => 0,
|
||||
'errors' => $errors
|
||||
];
|
||||
}
|
||||
@@ -2317,33 +2323,470 @@ class Cron
|
||||
|
||||
if ( !$sync_details )
|
||||
{
|
||||
// Daty historyczne: buduj ad_group_db_map z bazy i pobierz search terms za te date
|
||||
$ad_group_db_map = self::build_ad_group_db_map_from_db( $campaigns_db_map );
|
||||
|
||||
$search_terms_daily = self::sync_campaign_search_terms_daily( $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $as_of_date );
|
||||
$errors = array_merge( $errors, $search_terms_daily['errors'] );
|
||||
|
||||
return [
|
||||
'processed_records' => $processed,
|
||||
'ad_groups_synced' => 0,
|
||||
'search_terms_synced' => 0,
|
||||
'search_terms_synced' => (int) $search_terms_daily['count'],
|
||||
'keywords_synced' => 0,
|
||||
'negative_keywords_synced' => 0,
|
||||
'alerts_synced' => 0,
|
||||
'errors' => $errors
|
||||
];
|
||||
}
|
||||
|
||||
// Dzisiejsza data: najpierw sync ad_groups (DELETE + INSERT), potem search terms daily ze swiezym mapem
|
||||
$ad_groups_sync = self::sync_campaign_ad_groups_for_client( $campaigns_db_map, $customer_id, $api, $as_of_date );
|
||||
$search_terms_sync = self::sync_campaign_search_terms_for_client( $campaigns_db_map, $ad_groups_sync['ad_group_map'], $customer_id, $api, $as_of_date );
|
||||
$errors = array_merge( $errors, $ad_groups_sync['errors'] );
|
||||
|
||||
$search_terms_daily = self::sync_campaign_search_terms_daily( $campaigns_db_map, $ad_groups_sync['ad_group_map'], $customer_id, $api, $as_of_date );
|
||||
$errors = array_merge( $errors, $search_terms_daily['errors'] );
|
||||
|
||||
$aggregate_count = self::aggregate_campaign_search_terms_for_client( (int) $client['id'], $as_of_date );
|
||||
$keywords_sync = self::sync_campaign_keywords_for_client( $campaigns_db_map, $ad_groups_sync['ad_group_map'], $customer_id, $api, $as_of_date );
|
||||
$negative_keywords_sync = self::sync_campaign_negative_keywords_for_client( $campaigns_db_map, $ad_groups_sync['ad_group_map'], $customer_id, $api, $as_of_date );
|
||||
$alerts_sync = self::sync_product_campaign_alerts_for_client( $client, $campaigns_db_map, $ad_groups_sync['ad_group_map'], $customer_id, $api, $as_of_date );
|
||||
|
||||
$errors = array_merge( $errors, $ad_groups_sync['errors'], $search_terms_sync['errors'], $keywords_sync['errors'], $negative_keywords_sync['errors'] );
|
||||
$errors = array_merge( $errors, $keywords_sync['errors'], $negative_keywords_sync['errors'], $alerts_sync['errors'] );
|
||||
|
||||
return [
|
||||
'processed_records' => $processed,
|
||||
'ad_groups_synced' => (int) $ad_groups_sync['count'],
|
||||
'search_terms_synced' => (int) $search_terms_sync['count'],
|
||||
'search_terms_synced' => (int) $search_terms_daily['count'] + $aggregate_count,
|
||||
'keywords_synced' => (int) $keywords_sync['count'],
|
||||
'negative_keywords_synced' => (int) $negative_keywords_sync['count'],
|
||||
'alerts_synced' => (int) ( $alerts_sync['count'] ?? 0 ),
|
||||
'errors' => $errors
|
||||
];
|
||||
}
|
||||
|
||||
static private function sync_product_campaign_alerts_for_client( $client, $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $date_sync )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$campaign_db_ids = array_values( array_unique( array_map( 'intval', array_values( $campaigns_db_map ) ) ) );
|
||||
if ( empty( $campaign_db_ids ) )
|
||||
{
|
||||
return [ 'count' => 0, 'errors' => [] ];
|
||||
}
|
||||
|
||||
$date_sync = date( 'Y-m-d', strtotime( $date_sync ) );
|
||||
$client_id = (int) ( $client['id'] ?? 0 );
|
||||
$client_name = trim( (string) ( $client['name'] ?? '' ) );
|
||||
$merchant_account_id = preg_replace( '/\D+/', '', (string) ( $client['google_merchant_account_id'] ?? '' ) );
|
||||
|
||||
if ( $merchant_account_id === '' )
|
||||
{
|
||||
return [ 'count' => 0, 'errors' => [] ];
|
||||
}
|
||||
|
||||
$shopping_campaigns = $mdb -> query(
|
||||
"SELECT id, campaign_id, campaign_name
|
||||
FROM campaigns
|
||||
WHERE id IN (" . implode( ',', $campaign_db_ids ) . ")
|
||||
AND UPPER( TRIM( COALESCE( advertising_channel_type, '' ) ) ) = 'SHOPPING'"
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
if ( !is_array( $shopping_campaigns ) || empty( $shopping_campaigns ) )
|
||||
{
|
||||
return [ 'count' => 0, 'errors' => [] ];
|
||||
}
|
||||
|
||||
$shopping_campaign_external_ids = [];
|
||||
$shopping_campaign_names_by_db_id = [];
|
||||
|
||||
foreach ( $shopping_campaigns as $campaign_row )
|
||||
{
|
||||
$db_campaign_id = (int) ( $campaign_row['id'] ?? 0 );
|
||||
$external_campaign_id = (int) ( $campaign_row['campaign_id'] ?? 0 );
|
||||
|
||||
if ( $db_campaign_id <= 0 || $external_campaign_id <= 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$shopping_campaign_external_ids[ (string) $external_campaign_id ] = true;
|
||||
$shopping_campaign_names_by_db_id[ $db_campaign_id ] = trim( (string) ( $campaign_row['campaign_name'] ?? '' ) );
|
||||
}
|
||||
|
||||
if ( empty( $shopping_campaign_external_ids ) )
|
||||
{
|
||||
return [ 'count' => 0, 'errors' => [] ];
|
||||
}
|
||||
|
||||
$shopping_ad_groups_rows = $mdb -> query(
|
||||
"SELECT
|
||||
c.id AS campaign_db_id,
|
||||
c.campaign_id AS campaign_external_id,
|
||||
c.campaign_name,
|
||||
ag.id AS ad_group_db_id,
|
||||
ag.ad_group_id AS ad_group_external_id,
|
||||
ag.ad_group_name,
|
||||
ag.clicks_30,
|
||||
ag.clicks_all_time
|
||||
FROM campaign_ad_groups AS ag
|
||||
INNER JOIN campaigns AS c ON c.id = ag.campaign_id
|
||||
WHERE c.id IN (" . implode( ',', array_keys( $shopping_campaign_names_by_db_id ) ) . ")
|
||||
AND ag.ad_group_id > 0"
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$shopping_ad_groups_by_scope = [];
|
||||
foreach ( (array) $shopping_ad_groups_rows as $ag_row )
|
||||
{
|
||||
$campaign_external_id = (int) ( $ag_row['campaign_external_id'] ?? 0 );
|
||||
$ad_group_external_id = (int) ( $ag_row['ad_group_external_id'] ?? 0 );
|
||||
if ( $campaign_external_id <= 0 || $ad_group_external_id <= 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$scope_key = $campaign_external_id . '|' . $ad_group_external_id;
|
||||
$shopping_ad_groups_by_scope[ $scope_key ] = $ag_row;
|
||||
}
|
||||
|
||||
$ad_groups_offer_ids = $api -> get_shopping_ad_group_offer_ids( $customer_id );
|
||||
if ( $ad_groups_offer_ids === false )
|
||||
{
|
||||
$ad_groups_offer_ids = $api -> get_shopping_ad_group_offer_ids_from_performance( $customer_id );
|
||||
}
|
||||
|
||||
if ( $ad_groups_offer_ids === false )
|
||||
{
|
||||
$ad_groups_offer_ids = self::get_shopping_ad_group_offer_ids_from_history( $client_id, array_keys( $shopping_campaign_names_by_db_id ) );
|
||||
}
|
||||
|
||||
if ( !is_array( $ad_groups_offer_ids ) || empty( $ad_groups_offer_ids ) )
|
||||
{
|
||||
return [ 'count' => 0, 'errors' => [] ];
|
||||
}
|
||||
|
||||
$offer_ids_to_verify = [];
|
||||
$candidate_rows = [];
|
||||
|
||||
foreach ( $ad_groups_offer_ids as $row )
|
||||
{
|
||||
$campaign_external_id = (string) ( (int) ( $row['campaign_id'] ?? 0 ) );
|
||||
$ad_group_external_id = (string) ( (int) ( $row['ad_group_id'] ?? 0 ) );
|
||||
$offer_ids = array_values( array_unique( array_filter( array_map( function( $item )
|
||||
{
|
||||
return trim( (string) $item );
|
||||
}, (array) ( $row['offer_ids'] ?? [] ) ) ) ) );
|
||||
|
||||
if ( $campaign_external_id === '0' || $ad_group_external_id === '0' || empty( $offer_ids ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( !isset( $shopping_campaign_external_ids[ $campaign_external_id ] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$scope_key = $campaign_external_id . '|' . $ad_group_external_id;
|
||||
$candidate_rows[ $scope_key ] = [
|
||||
'campaign_external_id' => (int) $campaign_external_id,
|
||||
'ad_group_external_id' => (int) $ad_group_external_id,
|
||||
'campaign_name' => trim( (string) ( $row['campaign_name'] ?? '' ) ),
|
||||
'ad_group_name' => trim( (string) ( $row['ad_group_name'] ?? '' ) ),
|
||||
'offer_ids' => $offer_ids
|
||||
];
|
||||
|
||||
foreach ( $offer_ids as $offer_id )
|
||||
{
|
||||
$offer_ids_to_verify[ $offer_id ] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$merchant_items_map = [];
|
||||
if ( !empty( $offer_ids_to_verify ) )
|
||||
{
|
||||
$merchant_items_map = $api -> get_merchant_products_for_offer_ids( $merchant_account_id, array_keys( $offer_ids_to_verify ) );
|
||||
if ( $merchant_items_map === false )
|
||||
{
|
||||
$merchant_items_map = [];
|
||||
}
|
||||
}
|
||||
|
||||
if ( !is_array( $merchant_items_map ) )
|
||||
{
|
||||
$merchant_items_map = [];
|
||||
}
|
||||
|
||||
$inserted = 0;
|
||||
|
||||
$insert_alert = function( $alert_type, $campaign_external_id, $ad_group_external_id, $db_campaign_id, $db_ad_group_id, $message, $meta ) use ( $mdb, $client_id, $date_sync )
|
||||
{
|
||||
$existing_id = (int) $mdb -> get( 'campaign_alerts', 'id', [
|
||||
'AND' => [
|
||||
'client_id' => $client_id,
|
||||
'campaign_external_id' => (int) $campaign_external_id,
|
||||
'ad_group_external_id' => (int) $ad_group_external_id,
|
||||
'alert_type' => (string) $alert_type,
|
||||
'date_detected' => $date_sync
|
||||
]
|
||||
] );
|
||||
|
||||
if ( $existing_id > 0 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$mdb -> insert( 'campaign_alerts', [
|
||||
'client_id' => $client_id,
|
||||
'campaign_id' => (int) $db_campaign_id > 0 ? (int) $db_campaign_id : null,
|
||||
'campaign_external_id' => (int) $campaign_external_id,
|
||||
'ad_group_id' => (int) $db_ad_group_id > 0 ? (int) $db_ad_group_id : null,
|
||||
'ad_group_external_id' => (int) $ad_group_external_id,
|
||||
'alert_type' => (string) $alert_type,
|
||||
'message' => (string) $message,
|
||||
'meta_json' => json_encode( (array) $meta, JSON_UNESCAPED_UNICODE ),
|
||||
'date_detected' => $date_sync,
|
||||
'date_add' => date( 'Y-m-d H:i:s' )
|
||||
] );
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
foreach ( $candidate_rows as $scope_key => $row )
|
||||
{
|
||||
$campaign_external_id = (int) $row['campaign_external_id'];
|
||||
$ad_group_external_id = (int) $row['ad_group_external_id'];
|
||||
$offer_ids = (array) $row['offer_ids'];
|
||||
|
||||
if ( empty( $offer_ids ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$active_offer_count = 0;
|
||||
$orphaned_offer_ids = [];
|
||||
foreach ( $offer_ids as $offer_id )
|
||||
{
|
||||
if ( isset( $merchant_items_map[ $offer_id ] ) )
|
||||
{
|
||||
$active_offer_count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
$orphaned_offer_ids[] = $offer_id;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $active_offer_count > 0 && empty( $orphaned_offer_ids ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$db_campaign_id = (int) ( $campaigns_db_map[ (string) $campaign_external_id ] ?? 0 );
|
||||
$db_ad_group_id = (int) ( $ad_group_db_map[ (string) $campaign_external_id . '|' . (string) $ad_group_external_id ] ?? 0 );
|
||||
|
||||
$campaign_name = trim( (string) ( $row['campaign_name'] ?? '' ) );
|
||||
if ( $campaign_name === '' )
|
||||
{
|
||||
$campaign_name = trim( (string) ( $shopping_campaign_names_by_db_id[ $db_campaign_id ] ?? '' ) );
|
||||
}
|
||||
if ( $campaign_name === '' )
|
||||
{
|
||||
$campaign_name = 'Kampania #' . $campaign_external_id;
|
||||
}
|
||||
|
||||
$ad_group_name = trim( (string) ( $row['ad_group_name'] ?? '' ) );
|
||||
if ( $ad_group_name === '' )
|
||||
{
|
||||
$ad_group_name = 'Grupa reklam #' . $ad_group_external_id;
|
||||
}
|
||||
|
||||
if ( $active_offer_count === 0 )
|
||||
{
|
||||
$message = 'Brak aktywnych produktów w Merchant Center. Grupa reklam to "' . $ad_group_name . '" w kampanii "' . $campaign_name . '" na koncie klienta "' . $client_name . '".';
|
||||
|
||||
if ( $insert_alert(
|
||||
'ad_group_without_active_product',
|
||||
$campaign_external_id,
|
||||
$ad_group_external_id,
|
||||
$db_campaign_id,
|
||||
$db_ad_group_id,
|
||||
$message,
|
||||
[
|
||||
'offer_ids' => $offer_ids,
|
||||
'merchant_account_id' => $merchant_account_id,
|
||||
'source' => 'cron_campaigns_sync'
|
||||
]
|
||||
) )
|
||||
{
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
if ( !empty( $orphaned_offer_ids ) && $active_offer_count > 0 )
|
||||
{
|
||||
$orphaned_list = implode( ', ', array_slice( $orphaned_offer_ids, 0, 10 ) );
|
||||
if ( count( $orphaned_offer_ids ) > 10 )
|
||||
{
|
||||
$orphaned_list .= ' (i ' . ( count( $orphaned_offer_ids ) - 10 ) . ' więcej)';
|
||||
}
|
||||
|
||||
$message = count( $orphaned_offer_ids ) . ' osieroconych produktów (brak w MC), aktywnych: ' . $active_offer_count . '. Grupa reklam to "' . $ad_group_name . '" w kampanii "' . $campaign_name . '" na koncie klienta "' . $client_name . '". Osierocone ID: ' . $orphaned_list . '.';
|
||||
|
||||
if ( $insert_alert(
|
||||
'ad_group_with_orphaned_offers',
|
||||
$campaign_external_id,
|
||||
$ad_group_external_id,
|
||||
$db_campaign_id,
|
||||
$db_ad_group_id,
|
||||
$message,
|
||||
[
|
||||
'orphaned_offer_ids' => $orphaned_offer_ids,
|
||||
'active_offer_count' => $active_offer_count,
|
||||
'total_offer_count' => count( $offer_ids ),
|
||||
'merchant_account_id' => $merchant_account_id,
|
||||
'source' => 'cron_campaigns_sync'
|
||||
]
|
||||
) )
|
||||
{
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $shopping_ad_groups_by_scope as $scope_key => $ag_row )
|
||||
{
|
||||
if ( isset( $candidate_rows[ $scope_key ] ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$campaign_external_id = (int) ( $ag_row['campaign_external_id'] ?? 0 );
|
||||
$ad_group_external_id = (int) ( $ag_row['ad_group_external_id'] ?? 0 );
|
||||
$db_campaign_id = (int) ( $ag_row['campaign_db_id'] ?? 0 );
|
||||
$db_ad_group_id = (int) ( $ag_row['ad_group_db_id'] ?? 0 );
|
||||
|
||||
if ( $campaign_external_id <= 0 || $ad_group_external_id <= 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$campaign_name = trim( (string) ( $ag_row['campaign_name'] ?? '' ) );
|
||||
if ( $campaign_name === '' )
|
||||
{
|
||||
$campaign_name = 'Kampania #' . $campaign_external_id;
|
||||
}
|
||||
|
||||
$ad_group_name = trim( (string) ( $ag_row['ad_group_name'] ?? '' ) );
|
||||
if ( $ad_group_name === '' )
|
||||
{
|
||||
$ad_group_name = 'Grupa reklam #' . $ad_group_external_id;
|
||||
}
|
||||
|
||||
$message = 'Brak wykrytego przypisanego produktu. Grupa reklam to "' . $ad_group_name . '" w kampanii "' . $campaign_name . '" na koncie klienta "' . $client_name . '".';
|
||||
|
||||
if ( $insert_alert(
|
||||
'ad_group_without_detected_product',
|
||||
$campaign_external_id,
|
||||
$ad_group_external_id,
|
||||
$db_campaign_id,
|
||||
$db_ad_group_id,
|
||||
$message,
|
||||
[
|
||||
'merchant_account_id' => $merchant_account_id,
|
||||
'clicks_30' => (int) ( $ag_row['clicks_30'] ?? 0 ),
|
||||
'clicks_all_time' => (int) ( $ag_row['clicks_all_time'] ?? 0 ),
|
||||
'source' => 'cron_campaigns_sync_missing_mapping'
|
||||
]
|
||||
) )
|
||||
{
|
||||
$inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'count' => $inserted,
|
||||
'errors' => []
|
||||
];
|
||||
}
|
||||
|
||||
static private function get_shopping_ad_group_offer_ids_from_history( $client_id, $shopping_campaign_db_ids )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$client_id = (int) $client_id;
|
||||
$shopping_campaign_db_ids = array_values( array_unique( array_map( 'intval', (array) $shopping_campaign_db_ids ) ) );
|
||||
|
||||
if ( $client_id <= 0 || empty( $shopping_campaign_db_ids ) )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$campaign_ids_sql = implode( ',', $shopping_campaign_db_ids );
|
||||
|
||||
$rows = $mdb -> query(
|
||||
"SELECT
|
||||
c.id AS campaign_db_id,
|
||||
c.campaign_id AS campaign_external_id,
|
||||
c.campaign_name,
|
||||
ag.id AS ad_group_db_id,
|
||||
ag.ad_group_id AS ad_group_external_id,
|
||||
ag.ad_group_name,
|
||||
p.offer_id
|
||||
FROM campaign_ad_groups AS ag
|
||||
INNER JOIN campaigns AS c ON c.id = ag.campaign_id
|
||||
INNER JOIN products_history AS ph ON ph.campaign_id = c.id AND ph.ad_group_id = ag.id
|
||||
INNER JOIN products AS p ON p.id = ph.product_id
|
||||
WHERE c.client_id = :client_id
|
||||
AND c.id IN (" . $campaign_ids_sql . ")
|
||||
AND ag.ad_group_id > 0
|
||||
AND TRIM( COALESCE( p.offer_id, '' ) ) <> ''",
|
||||
[ ':client_id' => $client_id ]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
if ( !is_array( $rows ) || empty( $rows ) )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$scopes = [];
|
||||
|
||||
foreach ( $rows as $row )
|
||||
{
|
||||
$campaign_external_id = (int) ( $row['campaign_external_id'] ?? 0 );
|
||||
$ad_group_external_id = (int) ( $row['ad_group_external_id'] ?? 0 );
|
||||
$offer_id = trim( (string) ( $row['offer_id'] ?? '' ) );
|
||||
|
||||
if ( $campaign_external_id <= 0 || $ad_group_external_id <= 0 || $offer_id === '' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$scope_key = $campaign_external_id . '|' . $ad_group_external_id;
|
||||
|
||||
if ( !isset( $scopes[ $scope_key ] ) )
|
||||
{
|
||||
$scopes[ $scope_key ] = [
|
||||
'campaign_id' => $campaign_external_id,
|
||||
'campaign_name' => trim( (string) ( $row['campaign_name'] ?? '' ) ),
|
||||
'ad_group_id' => $ad_group_external_id,
|
||||
'ad_group_name' => trim( (string) ( $row['ad_group_name'] ?? '' ) ),
|
||||
'offer_ids' => []
|
||||
];
|
||||
}
|
||||
|
||||
$scopes[ $scope_key ]['offer_ids'][ $offer_id ] = true;
|
||||
}
|
||||
|
||||
foreach ( $scopes as &$scope )
|
||||
{
|
||||
$scope['offer_ids'] = array_values( array_keys( (array) $scope['offer_ids'] ) );
|
||||
}
|
||||
unset( $scope );
|
||||
|
||||
return array_values( $scopes );
|
||||
}
|
||||
|
||||
static private function sync_campaign_ad_groups_for_client( $campaigns_db_map, $customer_id, $api, $date_sync )
|
||||
{
|
||||
global $mdb;
|
||||
@@ -2402,10 +2845,9 @@ class Cron
|
||||
$map_all_time[ $campaign_external_id . '|' . $ad_group_external_id ] = $row;
|
||||
}
|
||||
|
||||
$mdb -> delete( 'campaign_ad_groups', [ 'campaign_id' => $campaign_db_ids ] );
|
||||
|
||||
$keys = array_values( array_unique( array_merge( array_keys( $map_30 ), array_keys( $map_all_time ) ) ) );
|
||||
$ad_group_db_map = [];
|
||||
$seen_db_ids = [];
|
||||
$count = 0;
|
||||
|
||||
foreach ( $keys as $key )
|
||||
@@ -2429,9 +2871,7 @@ class Cron
|
||||
$ad_group_name = 'Ad group #' . $ad_group_external_id;
|
||||
}
|
||||
|
||||
$mdb -> insert( 'campaign_ad_groups', [
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => (int) $ad_group_external_id,
|
||||
$data = [
|
||||
'ad_group_name' => $ad_group_name,
|
||||
'impressions_30' => (int) ( $row_30['impressions'] ?? 0 ),
|
||||
'clicks_30' => (int) ( $row_30['clicks'] ?? 0 ),
|
||||
@@ -2446,16 +2886,44 @@ class Cron
|
||||
'conversion_value_all_time' => (float) ( $row_all_time['conversion_value'] ?? 0 ),
|
||||
'roas_all_time' => (float) ( $row_all_time['roas'] ?? 0 ),
|
||||
'date_sync' => $date_sync
|
||||
] );
|
||||
];
|
||||
|
||||
$db_ad_group_id = (int) $mdb -> id();
|
||||
// Upsert: zachowaj istniejace DB ID (nie kasuj, bo CASCADE usunalby historie)
|
||||
$existing_id = (int) $mdb -> get( 'campaign_ad_groups', 'id', [ 'AND' => [
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => (int) $ad_group_external_id
|
||||
] ] );
|
||||
|
||||
if ( $existing_id > 0 )
|
||||
{
|
||||
$mdb -> update( 'campaign_ad_groups', $data, [ 'id' => $existing_id ] );
|
||||
$db_ad_group_id = $existing_id;
|
||||
}
|
||||
else
|
||||
{
|
||||
$data['campaign_id'] = $db_campaign_id;
|
||||
$data['ad_group_id'] = (int) $ad_group_external_id;
|
||||
$mdb -> insert( 'campaign_ad_groups', $data );
|
||||
$db_ad_group_id = (int) $mdb -> id();
|
||||
}
|
||||
if ( $db_ad_group_id > 0 )
|
||||
{
|
||||
$ad_group_db_map[ $key ] = $db_ad_group_id;
|
||||
$seen_db_ids[] = $db_ad_group_id;
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
// Usun ad_groups ktore nie pojawiaja sie juz w API (zachowaj PMax placeholder ad_group_id=0)
|
||||
if ( !empty( $seen_db_ids ) && !empty( $campaign_db_ids ) )
|
||||
{
|
||||
$mdb -> delete( 'campaign_ad_groups', [ 'AND' => [
|
||||
'campaign_id' => $campaign_db_ids,
|
||||
'id[!]' => $seen_db_ids,
|
||||
'ad_group_id[!]' => 0
|
||||
] ] );
|
||||
}
|
||||
|
||||
return [ 'count' => $count, 'ad_group_map' => $ad_group_db_map, 'errors' => [] ];
|
||||
}
|
||||
|
||||
@@ -2587,6 +3055,243 @@ class Cron
|
||||
return [ 'count' => $count, 'errors' => [] ];
|
||||
}
|
||||
|
||||
static private function build_ad_group_db_map_from_db( $campaigns_db_map )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$ad_group_db_map = [];
|
||||
$campaign_db_ids = array_values( array_unique( array_map( 'intval', array_values( $campaigns_db_map ) ) ) );
|
||||
|
||||
if ( empty( $campaign_db_ids ) )
|
||||
{
|
||||
return $ad_group_db_map;
|
||||
}
|
||||
|
||||
$ag_rows = $mdb -> query(
|
||||
"SELECT id, campaign_id, ad_group_id FROM campaign_ad_groups WHERE campaign_id IN (" . implode( ',', $campaign_db_ids ) . ")"
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$reverse_campaign_map = array_flip( $campaigns_db_map );
|
||||
|
||||
if ( is_array( $ag_rows ) )
|
||||
{
|
||||
foreach ( $ag_rows as $ag_row )
|
||||
{
|
||||
$ext_campaign_id = $reverse_campaign_map[ (int) $ag_row['campaign_id'] ] ?? '';
|
||||
if ( $ext_campaign_id !== '' )
|
||||
{
|
||||
$ad_group_db_map[ $ext_campaign_id . '|' . $ag_row['ad_group_id'] ] = (int) $ag_row['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $ad_group_db_map;
|
||||
}
|
||||
|
||||
static private function sync_campaign_search_terms_daily( $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $date )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$date = date( 'Y-m-d', strtotime( $date ) );
|
||||
|
||||
$campaign_db_ids = array_values( array_unique( array_map( 'intval', array_values( $campaigns_db_map ) ) ) );
|
||||
if ( empty( $campaign_db_ids ) )
|
||||
{
|
||||
return [ 'count' => 0, 'errors' => [] ];
|
||||
}
|
||||
|
||||
$terms = $api -> get_search_terms_for_date( $customer_id, $date );
|
||||
if ( $terms === false )
|
||||
{
|
||||
$last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
|
||||
return [ 'count' => 0, 'errors' => [ 'Blad pobierania fraz wyszukiwanych za ' . $date . ': ' . $last_err ] ];
|
||||
}
|
||||
|
||||
if ( !is_array( $terms ) )
|
||||
{
|
||||
$terms = [];
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ( $terms as $row )
|
||||
{
|
||||
$campaign_external_id = isset( $row['campaign_id'] ) ? (string) $row['campaign_id'] : '';
|
||||
$ad_group_external_id = isset( $row['ad_group_id'] ) ? (string) $row['ad_group_id'] : '';
|
||||
$search_term = trim( (string) ( $row['search_term'] ?? '' ) );
|
||||
|
||||
if ( $campaign_external_id === '' || $search_term === '' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$clicks = (int) ( $row['clicks'] ?? 0 );
|
||||
if ( $clicks <= 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$db_campaign_id = (int) ( $campaigns_db_map[ $campaign_external_id ] ?? 0 );
|
||||
$db_ad_group_id = (int) ( $ad_group_db_map[ $campaign_external_id . '|' . $ad_group_external_id ] ?? 0 );
|
||||
|
||||
if ( $db_campaign_id > 0 && $db_ad_group_id <= 0 && $ad_group_external_id === '0' )
|
||||
{
|
||||
$db_ad_group_id = self::ensure_campaign_level_ad_group( $db_campaign_id, $date );
|
||||
if ( $db_ad_group_id > 0 )
|
||||
{
|
||||
$ad_group_db_map[ $campaign_external_id . '|0' ] = $db_ad_group_id;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $db_campaign_id <= 0 || $db_ad_group_id <= 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = [
|
||||
'impressions' => (int) ( $row['impressions'] ?? 0 ),
|
||||
'clicks' => $clicks,
|
||||
'cost' => (float) ( $row['cost'] ?? 0 ),
|
||||
'conversions' => (float) ( $row['conversions'] ?? 0 ),
|
||||
'conversion_value' => (float) ( $row['conversion_value'] ?? 0 ),
|
||||
];
|
||||
|
||||
$existing_id = (int) $mdb -> get( 'campaign_search_terms_history', 'id', [ 'AND' => [
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $db_ad_group_id,
|
||||
'search_term' => $search_term,
|
||||
'date_add' => $date
|
||||
] ] );
|
||||
|
||||
if ( $existing_id > 0 )
|
||||
{
|
||||
$mdb -> update( 'campaign_search_terms_history', $data, [ 'id' => $existing_id ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$data['campaign_id'] = $db_campaign_id;
|
||||
$data['ad_group_id'] = $db_ad_group_id;
|
||||
$data['search_term'] = $search_term;
|
||||
$data['date_add'] = $date;
|
||||
$mdb -> insert( 'campaign_search_terms_history', $data );
|
||||
}
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
return [ 'count' => $count, 'errors' => [] ];
|
||||
}
|
||||
|
||||
static private function aggregate_campaign_search_terms_for_client( $client_id, $date_sync )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$client_id = (int) $client_id;
|
||||
$date_sync = date( 'Y-m-d', strtotime( $date_sync ) );
|
||||
$date_30_ago = date( 'Y-m-d', strtotime( $date_sync . ' -30 days' ) );
|
||||
|
||||
$campaign_db_ids = $mdb -> select( 'campaigns', 'id', [ 'client_id' => $client_id ] );
|
||||
if ( empty( $campaign_db_ids ) )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
$ids_list = implode( ',', array_map( 'intval', $campaign_db_ids ) );
|
||||
|
||||
$rows_30 = $mdb -> query(
|
||||
"SELECT campaign_id, ad_group_id, search_term,
|
||||
SUM(impressions) AS impressions,
|
||||
SUM(clicks) AS clicks,
|
||||
SUM(cost) AS cost,
|
||||
SUM(conversions) AS conversions,
|
||||
SUM(conversion_value) AS conversion_value
|
||||
FROM campaign_search_terms_history
|
||||
WHERE campaign_id IN ({$ids_list})
|
||||
AND date_add > :date_30_ago
|
||||
AND date_add <= :date_sync
|
||||
GROUP BY campaign_id, ad_group_id, search_term
|
||||
HAVING SUM(clicks) > 0",
|
||||
[ ':date_30_ago' => $date_30_ago, ':date_sync' => $date_sync ]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$rows_all_time = $mdb -> query(
|
||||
"SELECT campaign_id, ad_group_id, search_term,
|
||||
SUM(impressions) AS impressions,
|
||||
SUM(clicks) AS clicks,
|
||||
SUM(cost) AS cost,
|
||||
SUM(conversions) AS conversions,
|
||||
SUM(conversion_value) AS conversion_value
|
||||
FROM campaign_search_terms_history
|
||||
WHERE campaign_id IN ({$ids_list})
|
||||
GROUP BY campaign_id, ad_group_id, search_term
|
||||
HAVING SUM(clicks) > 0",
|
||||
[]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$map_30 = [];
|
||||
if ( is_array( $rows_30 ) )
|
||||
{
|
||||
foreach ( $rows_30 as $r )
|
||||
{
|
||||
$key = $r['campaign_id'] . '|' . $r['ad_group_id'] . '|' . strtolower( $r['search_term'] );
|
||||
$map_30[ $key ] = $r;
|
||||
}
|
||||
}
|
||||
|
||||
$map_all_time = [];
|
||||
if ( is_array( $rows_all_time ) )
|
||||
{
|
||||
foreach ( $rows_all_time as $r )
|
||||
{
|
||||
$key = $r['campaign_id'] . '|' . $r['ad_group_id'] . '|' . strtolower( $r['search_term'] );
|
||||
$map_all_time[ $key ] = $r;
|
||||
}
|
||||
}
|
||||
|
||||
$mdb -> delete( 'campaign_search_terms', [ 'campaign_id' => array_map( 'intval', $campaign_db_ids ) ] );
|
||||
|
||||
$keys = array_values( array_unique( array_merge( array_keys( $map_30 ), array_keys( $map_all_time ) ) ) );
|
||||
$count = 0;
|
||||
|
||||
foreach ( $keys as $key )
|
||||
{
|
||||
$r30 = $map_30[ $key ] ?? null;
|
||||
$rall = $map_all_time[ $key ] ?? null;
|
||||
$ref = $r30 ?? $rall;
|
||||
|
||||
$cost_30 = (float) ( $r30['cost'] ?? 0 );
|
||||
$cv_30 = (float) ( $r30['conversion_value'] ?? 0 );
|
||||
$roas_30 = ( $cost_30 > 0 ) ? round( ( $cv_30 / $cost_30 ) * 100, 2 ) : 0;
|
||||
|
||||
$cost_all = (float) ( $rall['cost'] ?? 0 );
|
||||
$cv_all = (float) ( $rall['conversion_value'] ?? 0 );
|
||||
$roas_all = ( $cost_all > 0 ) ? round( ( $cv_all / $cost_all ) * 100, 2 ) : 0;
|
||||
|
||||
$mdb -> insert( 'campaign_search_terms', [
|
||||
'campaign_id' => (int) $ref['campaign_id'],
|
||||
'ad_group_id' => (int) $ref['ad_group_id'],
|
||||
'search_term' => $ref['search_term'],
|
||||
'impressions_30' => (int) ( $r30['impressions'] ?? 0 ),
|
||||
'clicks_30' => (int) ( $r30['clicks'] ?? 0 ),
|
||||
'cost_30' => $cost_30,
|
||||
'conversions_30' => (float) ( $r30['conversions'] ?? 0 ),
|
||||
'conversion_value_30' => $cv_30,
|
||||
'roas_30' => $roas_30,
|
||||
'impressions_all_time' => (int) ( $rall['impressions'] ?? 0 ),
|
||||
'clicks_all_time' => (int) ( $rall['clicks'] ?? 0 ),
|
||||
'cost_all_time' => $cost_all,
|
||||
'conversions_all_time' => (float) ( $rall['conversions'] ?? 0 ),
|
||||
'conversion_value_all_time' => $cv_all,
|
||||
'roas_all_time' => $roas_all,
|
||||
'date_sync' => $date_sync
|
||||
] );
|
||||
|
||||
$count++;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
static private function ensure_campaign_level_ad_group( $db_campaign_id, $date_sync )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
112
autoload/factory/class.CampaignAlerts.php
Normal file
112
autoload/factory/class.CampaignAlerts.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
namespace factory;
|
||||
|
||||
class CampaignAlerts
|
||||
{
|
||||
static public function get_alerts_count( $client_id = 0 )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$where = [];
|
||||
$client_id = (int) $client_id;
|
||||
|
||||
if ( $client_id > 0 )
|
||||
{
|
||||
$where['client_id'] = $client_id;
|
||||
}
|
||||
|
||||
return (int) $mdb -> count( 'campaign_alerts', $where );
|
||||
}
|
||||
|
||||
static public function get_unseen_count()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
return (int) $mdb -> count( 'campaign_alerts', [ 'unseen' => 1 ] );
|
||||
}
|
||||
|
||||
static public function mark_all_seen()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$mdb -> update( 'campaign_alerts', [ 'unseen' => 0 ], [ 'unseen' => 1 ] );
|
||||
}
|
||||
|
||||
static public function get_clients()
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
return $mdb -> select( 'clients', [ 'id', 'name' ], [
|
||||
'deleted' => 0,
|
||||
'ORDER' => [ 'name' => 'ASC' ]
|
||||
] );
|
||||
}
|
||||
|
||||
static public function get_alerts( $client_id = 0, $limit = 15, $offset = 0 )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$client_id = (int) $client_id;
|
||||
$limit = max( 1, (int) $limit );
|
||||
$offset = max( 0, (int) $offset );
|
||||
|
||||
$sql = 'SELECT
|
||||
ca.id,
|
||||
ca.client_id,
|
||||
ca.campaign_id,
|
||||
ca.campaign_external_id,
|
||||
ca.ad_group_id,
|
||||
ca.ad_group_external_id,
|
||||
ca.alert_type,
|
||||
ca.message,
|
||||
ca.meta_json,
|
||||
ca.date_detected,
|
||||
ca.date_add AS date_created,
|
||||
cl.name AS client_name,
|
||||
c.campaign_name,
|
||||
ag.ad_group_name
|
||||
FROM campaign_alerts AS ca
|
||||
LEFT JOIN clients AS cl ON cl.id = ca.client_id
|
||||
LEFT JOIN campaigns AS c ON c.id = ca.campaign_id
|
||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = ca.ad_group_id';
|
||||
|
||||
if ( $client_id > 0 )
|
||||
{
|
||||
$sql .= ' WHERE ca.client_id = :client_id';
|
||||
}
|
||||
|
||||
$sql .= ' ORDER BY ca.date_detected DESC, ca.id DESC LIMIT ' . $offset . ', ' . $limit;
|
||||
|
||||
$stmt = $mdb -> pdo -> prepare( $sql );
|
||||
|
||||
if ( !$stmt )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if ( $client_id > 0 )
|
||||
{
|
||||
$stmt -> bindValue( ':client_id', $client_id, \PDO::PARAM_INT );
|
||||
}
|
||||
|
||||
$ok = $stmt -> execute();
|
||||
|
||||
if ( !$ok )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $stmt -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
return is_array( $rows ) ? $rows : [];
|
||||
}
|
||||
|
||||
static public function delete_old_alerts( $days = 30 )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$mdb -> delete( 'campaign_alerts', [
|
||||
'date_detected[<]' => date( 'Y-m-d', strtotime( '-' . (int) $days . ' days' ) )
|
||||
] );
|
||||
}
|
||||
}
|
||||
@@ -2511,6 +2511,249 @@ class GoogleAdsApi
|
||||
return $this -> aggregate_ad_groups( $results );
|
||||
}
|
||||
|
||||
public function get_shopping_ad_group_offer_ids( $customer_id )
|
||||
{
|
||||
$query_variants = [
|
||||
"SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.listing_group.case_value FROM ad_group_criterion WHERE campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.advertising_channel_type = 'SHOPPING' AND ad_group_criterion.type = 'LISTING_GROUP'",
|
||||
"SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.listing_group.case_value.product_item FROM ad_group_criterion WHERE campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.advertising_channel_type = 'SHOPPING' AND ad_group_criterion.type = 'LISTING_GROUP'",
|
||||
"SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.listing_group.case_value.product_item.id FROM ad_group_criterion WHERE campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.advertising_channel_type = 'SHOPPING' AND ad_group_criterion.type = 'LISTING_GROUP'",
|
||||
"SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.listing_group.case_value.product_item.value FROM ad_group_criterion WHERE campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.advertising_channel_type = 'SHOPPING' AND ad_group_criterion.type = 'LISTING_GROUP'",
|
||||
"SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, product_group_view.resource_name, ad_group_criterion.listing_group.case_value FROM product_group_view WHERE campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.advertising_channel_type = 'SHOPPING'",
|
||||
"SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, product_group_view.resource_name, ad_group_criterion.listing_group.case_value.product_item FROM product_group_view WHERE campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.advertising_channel_type = 'SHOPPING'"
|
||||
];
|
||||
|
||||
$results = false;
|
||||
$last_error = '';
|
||||
$variant_errors = [];
|
||||
|
||||
foreach ( $query_variants as $index => $gaql )
|
||||
{
|
||||
$tmp = $this -> search_stream( $customer_id, $gaql );
|
||||
if ( is_array( $tmp ) )
|
||||
{
|
||||
$results = $tmp;
|
||||
break;
|
||||
}
|
||||
|
||||
$last_error = (string) self::get_setting( 'google_ads_last_error' );
|
||||
$variant_errors[] = 'V' . ( $index + 1 ) . ': ' . substr( $last_error, 0, 350 );
|
||||
}
|
||||
|
||||
if ( !is_array( $results ) )
|
||||
{
|
||||
$diag = implode( ' || ', array_filter( $variant_errors ) );
|
||||
$error_to_save = $diag !== '' ? $diag : $last_error;
|
||||
|
||||
if ( $error_to_save !== '' )
|
||||
{
|
||||
self::set_setting( 'google_ads_last_error', $error_to_save );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !is_array( $results ) )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$collect_scalar_values = function( $value ) use ( &$collect_scalar_values )
|
||||
{
|
||||
$collected = [];
|
||||
|
||||
if ( is_array( $value ) )
|
||||
{
|
||||
foreach ( $value as $nested )
|
||||
{
|
||||
$collected = array_merge( $collected, $collect_scalar_values( $nested ) );
|
||||
}
|
||||
return $collected;
|
||||
}
|
||||
|
||||
if ( is_scalar( $value ) )
|
||||
{
|
||||
$tmp = trim( (string) $value );
|
||||
if ( $tmp !== '' )
|
||||
{
|
||||
$collected[] = $tmp;
|
||||
}
|
||||
}
|
||||
|
||||
return $collected;
|
||||
};
|
||||
|
||||
$extract_offer_ids = function( $row ) use ( $collect_scalar_values )
|
||||
{
|
||||
$candidates = [];
|
||||
|
||||
$case_value = $row['productGroupView']['caseValue']
|
||||
?? $row['product_group_view']['case_value']
|
||||
?? $row['adGroupCriterion']['listingGroup']['caseValue']
|
||||
?? $row['ad_group_criterion']['listing_group']['case_value']
|
||||
?? [];
|
||||
|
||||
if ( is_array( $case_value ) )
|
||||
{
|
||||
if ( isset( $case_value['productItem'] ) )
|
||||
{
|
||||
$candidates = array_merge( $candidates, $collect_scalar_values( $case_value['productItem'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $case_value['product_item'] ) )
|
||||
{
|
||||
$candidates = array_merge( $candidates, $collect_scalar_values( $case_value['product_item'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $case_value['productItemId'] ) )
|
||||
{
|
||||
$candidates[] = trim( (string) $case_value['productItemId'] );
|
||||
}
|
||||
|
||||
if ( isset( $case_value['product_item_id'] ) )
|
||||
{
|
||||
$candidates[] = trim( (string) $case_value['product_item_id'] );
|
||||
}
|
||||
}
|
||||
|
||||
$direct_candidates = [
|
||||
$row['adGroupCriterion']['listingGroup']['caseValue']['productItem'] ?? null,
|
||||
$row['ad_group_criterion']['listing_group']['case_value']['product_item'] ?? null,
|
||||
$row['adGroupCriterion']['listingGroup']['caseValue']['productItemId'] ?? null,
|
||||
$row['ad_group_criterion']['listing_group']['case_value']['product_item_id'] ?? null,
|
||||
$row['productGroupView']['caseValue']['productItem'] ?? null,
|
||||
$row['product_group_view']['case_value']['product_item'] ?? null,
|
||||
];
|
||||
|
||||
foreach ( $direct_candidates as $dc )
|
||||
{
|
||||
if ( $dc === null )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidates = array_merge( $candidates, $collect_scalar_values( $dc ) );
|
||||
}
|
||||
|
||||
$candidates = array_values( array_unique( array_filter( array_map( function( $item )
|
||||
{
|
||||
return trim( (string) $item );
|
||||
}, $candidates ) ) ) );
|
||||
|
||||
return $candidates;
|
||||
};
|
||||
|
||||
$scopes = [];
|
||||
|
||||
foreach ( $results as $row )
|
||||
{
|
||||
$campaign_id = (int) ( $row['campaign']['id'] ?? $row['campaignId'] ?? 0 );
|
||||
$ad_group_id = (int) ( $row['adGroup']['id'] ?? $row['ad_group']['id'] ?? $row['adGroupId'] ?? 0 );
|
||||
|
||||
if ( $campaign_id <= 0 || $ad_group_id <= 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$offer_ids = $extract_offer_ids( $row );
|
||||
|
||||
if ( empty( $offer_ids ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$scope_key = $campaign_id . '|' . $ad_group_id;
|
||||
|
||||
if ( !isset( $scopes[ $scope_key ] ) )
|
||||
{
|
||||
$scopes[ $scope_key ] = [
|
||||
'campaign_id' => $campaign_id,
|
||||
'campaign_name' => trim( (string) ( $row['campaign']['name'] ?? '' ) ),
|
||||
'ad_group_id' => $ad_group_id,
|
||||
'ad_group_name' => trim( (string) ( $row['adGroup']['name'] ?? $row['ad_group']['name'] ?? '' ) ),
|
||||
'offer_ids' => []
|
||||
];
|
||||
}
|
||||
|
||||
foreach ( $offer_ids as $offer_id )
|
||||
{
|
||||
$scopes[ $scope_key ]['offer_ids'][ $offer_id ] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $scopes as &$scope )
|
||||
{
|
||||
$scope['offer_ids'] = array_values( array_keys( (array) $scope['offer_ids'] ) );
|
||||
}
|
||||
unset( $scope );
|
||||
|
||||
return array_values( $scopes );
|
||||
}
|
||||
|
||||
public function get_shopping_ad_group_offer_ids_from_performance( $customer_id )
|
||||
{
|
||||
$gaql = "SELECT "
|
||||
. "campaign.id, "
|
||||
. "campaign.name, "
|
||||
. "campaign.status, "
|
||||
. "campaign.advertising_channel_type, "
|
||||
. "ad_group.id, "
|
||||
. "ad_group.name, "
|
||||
. "ad_group.status, "
|
||||
. "segments.product_item_id, "
|
||||
. "metrics.impressions "
|
||||
. "FROM shopping_performance_view "
|
||||
. "WHERE campaign.status != 'REMOVED' "
|
||||
. "AND ad_group.status != 'REMOVED' "
|
||||
. "AND campaign.advertising_channel_type = 'SHOPPING'";
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql );
|
||||
if ( $results === false )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !is_array( $results ) )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$scopes = [];
|
||||
|
||||
foreach ( $results as $row )
|
||||
{
|
||||
$campaign_id = (int) ( $row['campaign']['id'] ?? 0 );
|
||||
$ad_group_id = (int) ( $row['adGroup']['id'] ?? 0 );
|
||||
$offer_id = trim( (string) ( $row['segments']['productItemId'] ?? '' ) );
|
||||
|
||||
if ( $campaign_id <= 0 || $ad_group_id <= 0 || $offer_id === '' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$scope_key = $campaign_id . '|' . $ad_group_id;
|
||||
|
||||
if ( !isset( $scopes[ $scope_key ] ) )
|
||||
{
|
||||
$scopes[ $scope_key ] = [
|
||||
'campaign_id' => $campaign_id,
|
||||
'campaign_name' => trim( (string) ( $row['campaign']['name'] ?? '' ) ),
|
||||
'ad_group_id' => $ad_group_id,
|
||||
'ad_group_name' => trim( (string) ( $row['adGroup']['name'] ?? '' ) ),
|
||||
'offer_ids' => []
|
||||
];
|
||||
}
|
||||
|
||||
$scopes[ $scope_key ]['offer_ids'][ $offer_id ] = true;
|
||||
}
|
||||
|
||||
foreach ( $scopes as &$scope )
|
||||
{
|
||||
$scope['offer_ids'] = array_values( array_keys( (array) $scope['offer_ids'] ) );
|
||||
}
|
||||
unset( $scope );
|
||||
|
||||
return array_values( $scopes );
|
||||
}
|
||||
|
||||
public function get_search_terms_30_days( $customer_id )
|
||||
{
|
||||
$gaql = "SELECT "
|
||||
@@ -2586,10 +2829,9 @@ class GoogleAdsApi
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM ad_group_criterion "
|
||||
. "FROM keyword_view "
|
||||
. "WHERE campaign.status != 'REMOVED' "
|
||||
. "AND ad_group.status != 'REMOVED' "
|
||||
. "AND ad_group_criterion.type = 'KEYWORD' "
|
||||
. "AND ad_group_criterion.negative = FALSE "
|
||||
. "AND campaign.advertising_channel_type = 'SEARCH' "
|
||||
. "AND metrics.clicks > 0 "
|
||||
@@ -2613,10 +2855,9 @@ class GoogleAdsApi
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM ad_group_criterion "
|
||||
. "FROM keyword_view "
|
||||
. "WHERE campaign.status != 'REMOVED' "
|
||||
. "AND ad_group.status != 'REMOVED' "
|
||||
. "AND ad_group_criterion.type = 'KEYWORD' "
|
||||
. "AND ad_group_criterion.negative = FALSE "
|
||||
. "AND campaign.advertising_channel_type = 'SEARCH' "
|
||||
. "AND metrics.clicks > 0";
|
||||
@@ -2670,6 +2911,64 @@ class GoogleAdsApi
|
||||
return $this -> aggregate_campaign_search_terms( $results );
|
||||
}
|
||||
|
||||
public function get_search_terms_for_date( $customer_id, $date )
|
||||
{
|
||||
$date = date( 'Y-m-d', strtotime( $date ) );
|
||||
|
||||
$gaql = "SELECT "
|
||||
. "campaign.id, "
|
||||
. "ad_group.id, "
|
||||
. "ad_group.name, "
|
||||
. "search_term_view.search_term, "
|
||||
. "metrics.impressions, "
|
||||
. "metrics.clicks, "
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM search_term_view "
|
||||
. "WHERE campaign.status != 'REMOVED' "
|
||||
. "AND ad_group.status != 'REMOVED' "
|
||||
. "AND metrics.clicks > 0 "
|
||||
. "AND segments.date = '{$date}'";
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql );
|
||||
if ( $results === false ) return false;
|
||||
|
||||
$terms = $this -> aggregate_search_terms( $results );
|
||||
|
||||
$pmax_terms = $this -> get_pmax_search_terms_for_date( $customer_id, $date );
|
||||
if ( $pmax_terms !== false && is_array( $pmax_terms ) && !empty( $pmax_terms ) )
|
||||
{
|
||||
$terms = array_merge( $terms, $pmax_terms );
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
private function get_pmax_search_terms_for_date( $customer_id, $date )
|
||||
{
|
||||
$date = date( 'Y-m-d', strtotime( $date ) );
|
||||
|
||||
$gaql = "SELECT "
|
||||
. "campaign.id, "
|
||||
. "campaign_search_term_view.search_term, "
|
||||
. "metrics.impressions, "
|
||||
. "metrics.clicks, "
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM campaign_search_term_view "
|
||||
. "WHERE campaign.status != 'REMOVED' "
|
||||
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX' "
|
||||
. "AND metrics.clicks > 0 "
|
||||
. "AND segments.date = '{$date}'";
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql );
|
||||
if ( $results === false ) return false;
|
||||
|
||||
return $this -> aggregate_campaign_search_terms( $results );
|
||||
}
|
||||
|
||||
public function get_negative_keywords( $customer_id )
|
||||
{
|
||||
$campaign_gaql = "SELECT "
|
||||
|
||||
@@ -15,6 +15,7 @@ class Site
|
||||
{
|
||||
$tpl -> user = $user;
|
||||
$tpl -> current_module = $current_module;
|
||||
$tpl -> campaign_alerts_count = \factory\CampaignAlerts::get_unseen_count();
|
||||
if ( $alert = \S::get_session( 'alert' ) )
|
||||
{
|
||||
$tpl -> alert = $alert;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -529,6 +529,22 @@ body.logged {
|
||||
}
|
||||
}
|
||||
|
||||
.badge-alerts-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 6px;
|
||||
margin-left: 8px;
|
||||
border-radius: 50%;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
background: $cWhite;
|
||||
color: $cPrimary;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid rgba($cWhite, 0.08);
|
||||
|
||||
19
migrations/013_campaign_alerts.sql
Normal file
19
migrations/013_campaign_alerts.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE IF NOT EXISTS `campaign_alerts` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`client_id` int(11) NOT NULL,
|
||||
`campaign_id` int(11) DEFAULT NULL,
|
||||
`campaign_external_id` bigint(20) DEFAULT NULL,
|
||||
`ad_group_id` int(11) DEFAULT NULL,
|
||||
`ad_group_external_id` bigint(20) DEFAULT NULL,
|
||||
`alert_type` varchar(120) NOT NULL,
|
||||
`message` text NOT NULL,
|
||||
`meta_json` text DEFAULT NULL,
|
||||
`date_detected` date NOT NULL,
|
||||
`date_add` datetime NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_alert_daily` (`client_id`,`campaign_external_id`,`ad_group_external_id`,`alert_type`,`date_detected`),
|
||||
KEY `idx_alert_date` (`date_detected`),
|
||||
KEY `idx_alert_client` (`client_id`),
|
||||
KEY `idx_alert_campaign` (`campaign_id`),
|
||||
KEY `idx_alert_ad_group` (`ad_group_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
26
migrations/014_campaign_search_terms_history.sql
Normal file
26
migrations/014_campaign_search_terms_history.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
-- Migracja: tabela historii dziennych fraz wyszukiwanych
|
||||
-- Opis: przejscie z modelu snapshot (30d + all-time) na dane dzienne z agregacja w bazie
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `campaign_search_terms_history` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`campaign_id` INT(11) NOT NULL,
|
||||
`ad_group_id` INT(11) NOT NULL,
|
||||
`search_term` VARCHAR(255) NOT NULL,
|
||||
`impressions` INT(11) NOT NULL DEFAULT 0,
|
||||
`clicks` INT(11) NOT NULL DEFAULT 0,
|
||||
`cost` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`conversions` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`conversion_value` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||
`date_add` DATE NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_search_terms_history` (`campaign_id`, `ad_group_id`, `search_term`, `date_add`),
|
||||
KEY `idx_search_terms_history_campaign_id` (`campaign_id`),
|
||||
KEY `idx_search_terms_history_ad_group_id` (`ad_group_id`),
|
||||
KEY `idx_search_terms_history_date_add` (`date_add`),
|
||||
CONSTRAINT `FK_search_terms_history_campaigns`
|
||||
FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `FK_search_terms_history_ad_groups`
|
||||
FOREIGN KEY (`ad_group_id`) REFERENCES `campaign_ad_groups` (`id`)
|
||||
ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
7
migrations/015_campaign_alerts_unseen.sql
Normal file
7
migrations/015_campaign_alerts_unseen.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- Migracja: flaga unseen dla alertow kampanii
|
||||
-- Opis: nowe alerty sa unseen=1, po wejsciu na strone alertow oznaczane jako unseen=0
|
||||
|
||||
ALTER TABLE `campaign_alerts`
|
||||
ADD COLUMN IF NOT EXISTS `unseen` TINYINT(1) NOT NULL DEFAULT 1 AFTER `meta_json`;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS `idx_alert_unseen` ON `campaign_alerts` (`unseen`);
|
||||
113
templates/campaign_alerts/main_view.php
Normal file
113
templates/campaign_alerts/main_view.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<div class="campaigns-page">
|
||||
<div class="campaigns-header">
|
||||
<h2><i class="fa-solid fa-triangle-exclamation"></i> Alerty</h2>
|
||||
</div>
|
||||
|
||||
<form method="get" action="/campaign_alerts" class="campaigns-filters" style="margin-bottom:16px;">
|
||||
<div class="filter-group">
|
||||
<label for="client_id"><i class="fa-solid fa-building"></i> Klient</label>
|
||||
<select id="client_id" name="client_id" class="form-control" onchange="this.form.submit()">
|
||||
<option value="">- wszyscy klienci -</option>
|
||||
<?php foreach ( $this -> clients as $client ): ?>
|
||||
<option value="<?= (int) $client['id']; ?>" <?= (int) $this -> selected_client_id === (int) $client['id'] ? 'selected' : ''; ?>>
|
||||
<?= htmlspecialchars( (string) $client['name'] ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="campaigns-table-wrap">
|
||||
<table class="table" id="campaign_alerts_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Data</th>
|
||||
<th>Klient</th>
|
||||
<th>Kampania</th>
|
||||
<th>Grupa reklam</th>
|
||||
<th>Komunikat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ( empty( $this -> alerts ) ): ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">Brak alertów.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ( $this -> alerts as $row ): ?>
|
||||
<?php
|
||||
$campaign_name = trim( (string) ( $row['campaign_name'] ?? '' ) );
|
||||
if ( $campaign_name === '' )
|
||||
{
|
||||
$campaign_name = 'Kampania #' . (int) ( $row['campaign_external_id'] ?? 0 );
|
||||
}
|
||||
|
||||
$ad_group_name = trim( (string) ( $row['ad_group_name'] ?? '' ) );
|
||||
if ( $ad_group_name === '' )
|
||||
{
|
||||
$ad_group_name = 'Grupa reklam #' . (int) ( $row['ad_group_external_id'] ?? 0 );
|
||||
}
|
||||
|
||||
$client_name = trim( (string) ( $row['client_name'] ?? '' ) );
|
||||
if ( $client_name === '' )
|
||||
{
|
||||
$client_name = 'Klient #' . (int) ( $row['client_id'] ?? 0 );
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td style="white-space:nowrap"><?= htmlspecialchars( (string) ( $row['date_detected'] ?? '' ) ); ?></td>
|
||||
<td><?= htmlspecialchars( $client_name ); ?></td>
|
||||
<td><?= htmlspecialchars( $campaign_name ); ?></td>
|
||||
<td><?= htmlspecialchars( $ad_group_name ); ?></td>
|
||||
<td><?= htmlspecialchars( (string) ( $row['message'] ?? '' ) ); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if ( (int) $this -> total_pages > 1 ): ?>
|
||||
<div class="dt-layout-row" style="display:flex !important;justify-content:flex-end;">
|
||||
<div class="dt-paging">
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
<?php
|
||||
$page = (int) $this -> page;
|
||||
$total_pages = (int) $this -> total_pages;
|
||||
$client_id = (int) $this -> selected_client_id;
|
||||
$qs = $client_id > 0 ? '&client_id=' . $client_id : '';
|
||||
?>
|
||||
<li class="page-item <?= $page <= 1 ? 'disabled' : ''; ?>">
|
||||
<a class="page-link" href="/campaign_alerts?page=<?= $page - 1; ?><?= $qs; ?>">«</a>
|
||||
</li>
|
||||
<?php
|
||||
$start_p = max( 1, $page - 2 );
|
||||
$end_p = min( $total_pages, $page + 2 );
|
||||
if ( $start_p > 1 ):
|
||||
?>
|
||||
<li class="page-item"><a class="page-link" href="/campaign_alerts?page=1<?= $qs; ?>">1</a></li>
|
||||
<?php if ( $start_p > 2 ): ?>
|
||||
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php for ( $i = $start_p; $i <= $end_p; $i++ ): ?>
|
||||
<li class="page-item <?= $i === $page ? 'active' : ''; ?>">
|
||||
<a class="page-link" href="/campaign_alerts?page=<?= $i; ?><?= $qs; ?>"><?= $i; ?></a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
<?php if ( $end_p < $total_pages ): ?>
|
||||
<?php if ( $end_p < $total_pages - 1 ): ?>
|
||||
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||||
<?php endif; ?>
|
||||
<li class="page-item"><a class="page-link" href="/campaign_alerts?page=<?= $total_pages; ?><?= $qs; ?>"><?= $total_pages; ?></a></li>
|
||||
<?php endif; ?>
|
||||
<li class="page-item <?= $page >= $total_pages ? 'disabled' : ''; ?>">
|
||||
<a class="page-link" href="/campaign_alerts?page=<?= $page + 1; ?><?= $qs; ?>">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,7 +16,7 @@
|
||||
<th>Merchant Account ID</th>
|
||||
<th>Dane od</th>
|
||||
<th style="width: 160px;">Sync</th>
|
||||
<th style="width: 120px; text-align: center;">Akcje</th>
|
||||
<th style="width: 160px; text-align: center;">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -49,8 +49,11 @@
|
||||
<td class="client-sync" data-sync-id="<?= $client['id']; ?>"><span class="text-muted">—</span></td>
|
||||
<td class="actions-cell">
|
||||
<?php if ( $client['google_ads_customer_id'] ): ?>
|
||||
<button type="button" class="btn-icon btn-icon-sync" onclick="syncClient(<?= $client['id']; ?>, this)" title="Pobierz dane z Google Ads">
|
||||
<i class="fa-solid fa-rotate"></i>
|
||||
<button type="button" class="btn-icon btn-icon-sync" onclick="syncClient(<?= $client['id']; ?>, 'campaigns', this)" title="Odśwież kampanie">
|
||||
<i class="fa-solid fa-bullhorn"></i>
|
||||
</button>
|
||||
<button type="button" class="btn-icon btn-icon-sync" onclick="syncClient(<?= $client['id']; ?>, 'products', this)" title="Odśwież produkty">
|
||||
<i class="fa-solid fa-box-open"></i>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn-icon btn-icon-edit" onclick="editClient(<?= $client['id']; ?>)" title="Edytuj">
|
||||
@@ -142,20 +145,23 @@ function editClient( id )
|
||||
} );
|
||||
}
|
||||
|
||||
function syncClient( id, btn )
|
||||
function syncClient( id, pipeline, btn )
|
||||
{
|
||||
var $btn = $( btn );
|
||||
var $icon = $btn.find( 'i' );
|
||||
var origClass = $icon.attr( 'class' );
|
||||
|
||||
$btn.prop( 'disabled', true );
|
||||
$icon.removeClass( 'fa-rotate' ).addClass( 'fa-spinner fa-spin' );
|
||||
$icon.attr( 'class', 'fa-solid fa-spinner fa-spin' );
|
||||
|
||||
$.post( '/clients/force_sync', { id: id }, function( response )
|
||||
var labels = { campaigns: 'kampanii', products: 'produktów' };
|
||||
|
||||
$.post( '/clients/force_sync', { id: id, pipeline: pipeline }, function( response )
|
||||
{
|
||||
var data = JSON.parse( response );
|
||||
|
||||
$btn.prop( 'disabled', false );
|
||||
$icon.removeClass( 'fa-spinner fa-spin' ).addClass( 'fa-rotate' );
|
||||
$icon.attr( 'class', origClass );
|
||||
|
||||
if ( data.success )
|
||||
{
|
||||
@@ -163,7 +169,7 @@ function syncClient( id, btn )
|
||||
|
||||
$.alert({
|
||||
title: 'Zakolejkowano',
|
||||
content: 'Klient zostal oznaczony do ponownej synchronizacji. Przy najblizszym uruchomieniu CRON dane kampanii i produktow zostana pobrane od nowa dla calego okna konwersji.',
|
||||
content: 'Synchronizacja ' + labels[ pipeline ] + ' zostala zakolejkowana. Dane zostana pobrane przy najblizszym uruchomieniu CRON.',
|
||||
type: 'green',
|
||||
autoClose: 'ok|3000'
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<script src="/libraries/framework/vendor/plugins/moment/pl.js"></script>
|
||||
<script src="/libraries/framework/vendor/plugins/datepicker/js/bootstrap-datetimepicker.js"></script>
|
||||
<script src="/libraries/framework/vendor/plugins/daterange/daterangepicker.js"></script>
|
||||
<script src="/libraries/adspro-dialog.js">
|
||||
<script src="/libraries/adspro-dialog.js"></script>
|
||||
<script src="/libraries/select2/js/select2.full.min.js"></script>
|
||||
<script src="/libraries/functions.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/2.1.7/css/dataTables.bootstrap5.min.css">
|
||||
@@ -37,7 +37,7 @@
|
||||
<body class="logged">
|
||||
<?php
|
||||
$module = $this -> current_module;
|
||||
$google_ads_modules = [ 'campaigns', 'campaign_terms', 'products', 'clients', 'xml_files' ];
|
||||
$google_ads_modules = [ 'campaigns', 'campaign_terms', 'products', 'campaign_alerts', 'clients', 'xml_files' ];
|
||||
$is_google_ads_module = in_array( $module, $google_ads_modules, true );
|
||||
?>
|
||||
<!-- Sidebar -->
|
||||
@@ -78,6 +78,15 @@
|
||||
<span>Produkty</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<?= $module === 'campaign_alerts' ? 'active' : '' ?>">
|
||||
<a href="/campaign_alerts">
|
||||
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||
<span>Alerty</span>
|
||||
<?php if ( (int) ( $this -> campaign_alerts_count ?? 0 ) > 0 ): ?>
|
||||
<span class="badge-alerts-count"><?= (int) $this -> campaign_alerts_count; ?></span>
|
||||
<?php endif; ?>
|
||||
</a>
|
||||
</li>
|
||||
<li class="<?= $module === 'clients' ? 'active' : '' ?>">
|
||||
<a href="/clients">
|
||||
<i class="fa-solid fa-building"></i>
|
||||
@@ -135,6 +144,7 @@
|
||||
'campaigns' => 'Kampanie',
|
||||
'campaign_terms' => 'Grupy i frazy',
|
||||
'products' => 'Produkty',
|
||||
'campaign_alerts' => 'Alerty',
|
||||
'clients' => 'Klienci',
|
||||
'xml_files' => 'Pliki XML',
|
||||
'allegro' => 'Allegro import',
|
||||
|
||||
Reference in New Issue
Block a user