feat: Enhance Google Ads API integration and add column visibility control in product view
- Updated GoogleAdsApi class to include new GAQL queries for Performance Max campaigns and fallback mechanisms. - Refactored data collection logic to handle ad groups and asset groups more effectively. - Modified get_campaigns_30_days and get_campaigns_all_time methods to accept an optional date parameter for improved date filtering. - Introduced a new UI feature in the products main view to allow users to toggle column visibility, enhancing user experience. - Implemented local storage functionality to remember user preferences for column visibility across sessions.
This commit is contained in:
@@ -14,6 +14,8 @@ class Cron
|
||||
}
|
||||
|
||||
$date = \S::get( 'date' ) ? date( 'Y-m-d', strtotime( \S::get( 'date' ) ) ) : date( 'Y-m-d', strtotime( '-1 days' ) );
|
||||
$conversion_window_days = self::get_conversion_window_days();
|
||||
$import_dates = self::build_backfill_dates( $date, $conversion_window_days );
|
||||
$client_id = (int) \S::get( 'client_id' );
|
||||
|
||||
if ( $client_id > 0 )
|
||||
@@ -38,6 +40,7 @@ class Cron
|
||||
'result' => empty( $sync['errors'] ) ? 'Synchronizacja produktow zakonczona.' : 'Synchronizacja produktow zakonczona z bledami.',
|
||||
'client_id' => (int) $client['id'],
|
||||
'date' => $date,
|
||||
'active_date' => $date,
|
||||
'phase' => 'single_full',
|
||||
'processed_products' => (int) $sync['processed_products'],
|
||||
'skipped' => (int) $sync['skipped'],
|
||||
@@ -58,13 +61,16 @@ class Cron
|
||||
}
|
||||
|
||||
$state_key = 'cron_products_pipeline_state';
|
||||
$state = self::get_products_pipeline_state( $state_key, $date, $client_ids );
|
||||
$state = self::get_products_pipeline_state( $state_key, $date, $client_ids, $import_dates );
|
||||
|
||||
if ( $state['phase'] === 'done' )
|
||||
{
|
||||
echo json_encode( [
|
||||
'result' => 'Pipeline cron_products jest zakonczony dla tej daty.',
|
||||
'date' => $state['import_date'],
|
||||
'result' => 'Pipeline cron_products jest zakonczony dla calego okna dat.',
|
||||
'date' => $date,
|
||||
'active_date' => $state['import_date'],
|
||||
'conversion_window_days' => (int) ( $state['conversion_window_days'] ?? $conversion_window_days ),
|
||||
'dates_synced' => $state['import_dates'] ?? $import_dates,
|
||||
'phase' => 'done',
|
||||
'total_clients' => count( $client_ids )
|
||||
] );
|
||||
@@ -80,7 +86,7 @@ class Cron
|
||||
$done_key = $phase_map[ $state['phase'] ] ?? null;
|
||||
if ( !$done_key )
|
||||
{
|
||||
$state = self::init_products_pipeline_state( $date, $client_ids );
|
||||
$state = self::init_products_pipeline_state( $date, $client_ids, $import_dates );
|
||||
$done_key = 'fetch_done_ids';
|
||||
}
|
||||
|
||||
@@ -93,7 +99,9 @@ class Cron
|
||||
|
||||
echo json_encode( [
|
||||
'result' => 'Faza zakonczona. Przejdz do nastepnej fazy kolejnym wywolaniem.',
|
||||
'date' => $state['import_date'],
|
||||
'date' => $date,
|
||||
'active_date' => $state['import_date'],
|
||||
'conversion_window_days' => (int) ( $state['conversion_window_days'] ?? $conversion_window_days ),
|
||||
'phase' => $state['phase'],
|
||||
'total_clients' => count( $client_ids )
|
||||
] );
|
||||
@@ -109,7 +117,10 @@ class Cron
|
||||
|
||||
$response = [
|
||||
'result' => '',
|
||||
'date' => $state['import_date'],
|
||||
'date' => $date,
|
||||
'active_date' => $state['import_date'],
|
||||
'conversion_window_days' => (int) ( $state['conversion_window_days'] ?? $conversion_window_days ),
|
||||
'dates_synced' => $state['import_dates'] ?? $import_dates,
|
||||
'phase' => $state['phase'],
|
||||
'client_id' => (int) $selected_client['id'],
|
||||
'errors' => []
|
||||
@@ -151,11 +162,24 @@ class Cron
|
||||
exit;
|
||||
}
|
||||
|
||||
static private function init_products_pipeline_state( $date, $client_ids )
|
||||
static private function init_products_pipeline_state( $date, $client_ids, $import_dates )
|
||||
{
|
||||
$import_dates = array_values( array_unique( array_map( function( $item )
|
||||
{
|
||||
return date( 'Y-m-d', strtotime( $item ) );
|
||||
}, (array) $import_dates ) ) );
|
||||
sort( $import_dates );
|
||||
|
||||
$anchor_date = date( 'Y-m-d', strtotime( $date ) );
|
||||
$initial_date = !empty( $import_dates ) ? $import_dates[0] : $anchor_date;
|
||||
|
||||
return [
|
||||
'import_date' => date( 'Y-m-d', strtotime( $date ) ),
|
||||
'clients_hash' => md5( implode( ',', $client_ids ) ),
|
||||
'anchor_date' => $anchor_date,
|
||||
'import_date' => $initial_date,
|
||||
'import_dates' => $import_dates,
|
||||
'current_date_index' => 0,
|
||||
'conversion_window_days' => count( $import_dates ),
|
||||
'clients_hash' => md5( $anchor_date . '|' . implode( ',', $client_ids ) . '|' . implode( ',', $import_dates ) ),
|
||||
'phase' => 'fetch',
|
||||
'fetch_done_ids' => [],
|
||||
'aggregate_30_done_ids' => [],
|
||||
@@ -163,24 +187,40 @@ class Cron
|
||||
];
|
||||
}
|
||||
|
||||
static private function get_products_pipeline_state( $state_key, $date, $client_ids )
|
||||
static private function get_products_pipeline_state( $state_key, $date, $client_ids, $import_dates )
|
||||
{
|
||||
$state_raw = self::get_setting_value( $state_key, '' );
|
||||
$state = json_decode( (string) $state_raw, true );
|
||||
|
||||
$expected_date = date( 'Y-m-d', strtotime( $date ) );
|
||||
$expected_hash = md5( implode( ',', $client_ids ) );
|
||||
$expected_dates = array_values( array_unique( array_map( function( $item )
|
||||
{
|
||||
return date( 'Y-m-d', strtotime( $item ) );
|
||||
}, (array) $import_dates ) ) );
|
||||
sort( $expected_dates );
|
||||
$expected_hash = md5( $expected_date . '|' . implode( ',', $client_ids ) . '|' . implode( ',', $expected_dates ) );
|
||||
|
||||
if ( !is_array( $state ) )
|
||||
{
|
||||
return self::init_products_pipeline_state( $expected_date, $client_ids );
|
||||
return self::init_products_pipeline_state( $expected_date, $client_ids, $expected_dates );
|
||||
}
|
||||
|
||||
if ( ( $state['import_date'] ?? '' ) !== $expected_date || ( $state['clients_hash'] ?? '' ) !== $expected_hash )
|
||||
if ( ( $state['anchor_date'] ?? '' ) !== $expected_date || ( $state['clients_hash'] ?? '' ) !== $expected_hash )
|
||||
{
|
||||
return self::init_products_pipeline_state( $expected_date, $client_ids );
|
||||
return self::init_products_pipeline_state( $expected_date, $client_ids, $expected_dates );
|
||||
}
|
||||
|
||||
if ( !isset( $state['import_dates'] ) || !is_array( $state['import_dates'] ) || empty( $state['import_dates'] ) )
|
||||
{
|
||||
$state['import_dates'] = $expected_dates;
|
||||
}
|
||||
if ( !isset( $state['current_date_index'] ) || !is_numeric( $state['current_date_index'] ) )
|
||||
{
|
||||
$state['current_date_index'] = 0;
|
||||
}
|
||||
$state['current_date_index'] = max( 0, min( count( $state['import_dates'] ) - 1, (int) $state['current_date_index'] ) );
|
||||
$state['import_date'] = $state['import_dates'][ $state['current_date_index'] ] ?? $expected_date;
|
||||
|
||||
foreach ( [ 'fetch_done_ids', 'aggregate_30_done_ids', 'aggregate_temp_done_ids' ] as $key )
|
||||
{
|
||||
if ( !isset( $state[ $key ] ) || !is_array( $state[ $key ] ) )
|
||||
@@ -217,6 +257,21 @@ class Cron
|
||||
return $state;
|
||||
}
|
||||
|
||||
$current_date_index = (int) ( $state['current_date_index'] ?? 0 );
|
||||
$import_dates = is_array( $state['import_dates'] ?? null ) ? $state['import_dates'] : [];
|
||||
$last_index = count( $import_dates ) - 1;
|
||||
|
||||
if ( $last_index >= 0 && $current_date_index < $last_index )
|
||||
{
|
||||
$state['current_date_index'] = $current_date_index + 1;
|
||||
$state['import_date'] = $import_dates[ $state['current_date_index'] ];
|
||||
$state['phase'] = 'fetch';
|
||||
$state['fetch_done_ids'] = [];
|
||||
$state['aggregate_30_done_ids'] = [];
|
||||
$state['aggregate_temp_done_ids'] = [];
|
||||
return $state;
|
||||
}
|
||||
|
||||
$state['phase'] = 'done';
|
||||
return $state;
|
||||
}
|
||||
@@ -542,22 +597,27 @@ class Cron
|
||||
return (int) $mdb -> id();
|
||||
}
|
||||
|
||||
static private function aggregate_products_history_30_for_client( $client_id, $date )
|
||||
static private function aggregate_products_history_30_for_client( $client_id, $date = null )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$client_id = (int) $client_id;
|
||||
$date = date( 'Y-m-d', strtotime( $date ) );
|
||||
$params = [ ':client_id' => $client_id ];
|
||||
$sql = 'SELECT DISTINCT ph.product_id, ph.campaign_id, ph.ad_group_id, ph.date_add
|
||||
FROM products_history AS ph
|
||||
INNER JOIN products AS p ON p.id = ph.product_id
|
||||
WHERE p.client_id = :client_id
|
||||
AND ph.updated = 1';
|
||||
|
||||
$rows = $mdb -> query(
|
||||
'SELECT DISTINCT ph.product_id, ph.campaign_id, ph.ad_group_id, ph.date_add
|
||||
FROM products_history AS ph
|
||||
INNER JOIN products AS p ON p.id = ph.product_id
|
||||
WHERE p.client_id = ' . $client_id . '
|
||||
AND ph.updated = 1
|
||||
AND ph.date_add = \'' . $date . '\'
|
||||
ORDER BY ph.product_id ASC, ph.campaign_id ASC, ph.ad_group_id ASC'
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
if ( $date )
|
||||
{
|
||||
$params[':date_add'] = date( 'Y-m-d', strtotime( $date ) );
|
||||
$sql .= ' AND ph.date_add = :date_add';
|
||||
}
|
||||
|
||||
$sql .= ' ORDER BY ph.date_add ASC, ph.product_id ASC, ph.campaign_id ASC, ph.ad_group_id ASC';
|
||||
|
||||
$rows = $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$processed = 0;
|
||||
foreach ( $rows as $row )
|
||||
@@ -1178,6 +1238,9 @@ class Cron
|
||||
exit;
|
||||
}
|
||||
|
||||
$sync_date = \S::get( 'date' ) ? date( 'Y-m-d', strtotime( \S::get( 'date' ) ) ) : date( 'Y-m-d' );
|
||||
$conversion_window_days = self::get_conversion_window_days();
|
||||
$sync_dates = self::build_backfill_dates( $sync_date, $conversion_window_days );
|
||||
$client_id = (int) \S::get( 'client_id' );
|
||||
|
||||
if ( $client_id > 0 )
|
||||
@@ -1194,11 +1257,13 @@ class Cron
|
||||
exit;
|
||||
}
|
||||
|
||||
$sync = self::sync_campaigns_for_client( $client, $api );
|
||||
$sync = self::sync_campaigns_for_client( $client, $api, $sync_date, true );
|
||||
|
||||
echo json_encode( [
|
||||
'result' => empty( $sync['errors'] ) ? 'Synchronizacja kampanii zakonczona.' : 'Synchronizacja kampanii zakonczona z bledami.',
|
||||
'client_id' => (int) $client['id'],
|
||||
'date' => $sync_date,
|
||||
'active_date' => $sync_date,
|
||||
'processed_records' => (int) $sync['processed_records'],
|
||||
'ad_groups_synced' => (int) ( $sync['ad_groups_synced'] ?? 0 ),
|
||||
'search_terms_synced' => (int) ( $sync['search_terms_synced'] ?? 0 ),
|
||||
@@ -1239,15 +1304,44 @@ class Cron
|
||||
|
||||
$client_ids = array_values( array_unique( $client_ids ) );
|
||||
|
||||
$window_state_key = 'cron_campaigns_window_state';
|
||||
$window_state = self::get_campaigns_window_state( $window_state_key, $sync_date, $sync_dates );
|
||||
self::save_campaigns_window_state( $window_state_key, $window_state );
|
||||
$active_sync_date = $window_state['sync_date'];
|
||||
$sync_details = ( $active_sync_date === $sync_date );
|
||||
|
||||
$state_key = 'cron_campaigns_state';
|
||||
$state = self::get_daily_cron_state( $state_key );
|
||||
$state = self::get_daily_cron_state( $state_key, $active_sync_date );
|
||||
$next_client_id = self::pick_next_client_id( $client_ids, $state['processed_ids'] );
|
||||
|
||||
if ( !$next_client_id )
|
||||
{
|
||||
$previous_index = (int) ( $window_state['current_date_index'] ?? 0 );
|
||||
$window_state = self::advance_campaigns_window_state( $window_state );
|
||||
$next_index = (int) ( $window_state['current_date_index'] ?? 0 );
|
||||
|
||||
if ( $next_index !== $previous_index )
|
||||
{
|
||||
self::save_campaigns_window_state( $window_state_key, $window_state );
|
||||
echo json_encode( [
|
||||
'result' => 'Wszyscy klienci dla aktualnego dnia zostali przetworzeni. Kolejne wywolanie przejdzie do nastepnego dnia.',
|
||||
'date' => $sync_date,
|
||||
'active_date' => $active_sync_date,
|
||||
'next_active_date' => $window_state['sync_date'],
|
||||
'conversion_window_days' => $conversion_window_days,
|
||||
'dates_synced' => $window_state['sync_dates'],
|
||||
'processed_clients' => count( array_intersect( $client_ids, $state['processed_ids'] ) ),
|
||||
'total_clients' => count( $client_ids )
|
||||
] );
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode( [
|
||||
'result' => 'Wszyscy klienci kampanii zostali juz dzis przetworzeni.',
|
||||
'date' => $state['date'],
|
||||
'result' => 'Wszyscy klienci kampanii zostali juz przetworzeni dla calego okna dat.',
|
||||
'date' => $sync_date,
|
||||
'active_date' => $active_sync_date,
|
||||
'conversion_window_days' => $conversion_window_days,
|
||||
'dates_synced' => $window_state['sync_dates'],
|
||||
'processed_clients' => count( array_intersect( $client_ids, $state['processed_ids'] ) ),
|
||||
'total_clients' => count( $client_ids )
|
||||
] );
|
||||
@@ -1261,18 +1355,22 @@ class Cron
|
||||
exit;
|
||||
}
|
||||
|
||||
$sync = self::sync_campaigns_for_client( $selected_client, $api );
|
||||
$sync = self::sync_campaigns_for_client( $selected_client, $api, $active_sync_date, $sync_details );
|
||||
|
||||
// Oznaczamy klienta jako przetworzonego rowniez po bledzie, aby nie zapetlac wywolan.
|
||||
$state['processed_ids'][] = (int) $next_client_id;
|
||||
$state['processed_ids'] = array_values( array_unique( array_map( 'intval', $state['processed_ids'] ) ) );
|
||||
self::save_daily_cron_state( $state_key, $state );
|
||||
self::save_daily_cron_state( $state_key, $state, $active_sync_date );
|
||||
|
||||
$processed_today = count( array_intersect( $client_ids, $state['processed_ids'] ) );
|
||||
|
||||
echo json_encode( [
|
||||
'result' => empty( $sync['errors'] ) ? 'Synchronizacja kampanii zakonczona.' : 'Synchronizacja kampanii zakonczona z bledami.',
|
||||
'client_id' => $next_client_id,
|
||||
'date' => $sync_date,
|
||||
'active_date' => $active_sync_date,
|
||||
'conversion_window_days' => $conversion_window_days,
|
||||
'dates_synced' => $window_state['sync_dates'],
|
||||
'processed_records' => (int) $sync['processed_records'],
|
||||
'ad_groups_synced' => (int) ( $sync['ad_groups_synced'] ?? 0 ),
|
||||
'search_terms_synced' => (int) ( $sync['search_terms_synced'] ?? 0 ),
|
||||
@@ -1284,17 +1382,18 @@ class Cron
|
||||
exit;
|
||||
}
|
||||
|
||||
static private function sync_campaigns_for_client( $client, $api )
|
||||
static private function sync_campaigns_for_client( $client, $api, $as_of_date = null, $sync_details = true )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$today = date( 'Y-m-d' );
|
||||
$as_of_date = $as_of_date ? date( 'Y-m-d', strtotime( $as_of_date ) ) : date( 'Y-m-d' );
|
||||
$sync_details = (bool) $sync_details;
|
||||
$processed = 0;
|
||||
$errors = [];
|
||||
$customer_id = $client['google_ads_customer_id'];
|
||||
$campaigns_db_map = [];
|
||||
|
||||
$campaigns_30 = $api -> get_campaigns_30_days( $customer_id );
|
||||
$campaigns_30 = $api -> get_campaigns_30_days( $customer_id, $as_of_date );
|
||||
if ( $campaigns_30 === false )
|
||||
{
|
||||
$last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
|
||||
@@ -1314,7 +1413,21 @@ class Cron
|
||||
$campaigns_30 = [];
|
||||
}
|
||||
|
||||
$campaigns_all_time = $api -> get_campaigns_all_time( $customer_id );
|
||||
$campaigns_all_time = $api -> get_campaigns_all_time( $customer_id, $as_of_date );
|
||||
if ( $campaigns_all_time === false )
|
||||
{
|
||||
$last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
|
||||
$errors[] = 'Blad pobierania danych all time dla klienta ' . $client['name'] . ' (ID: ' . $customer_id . '): ' . $last_err;
|
||||
|
||||
return [
|
||||
'processed_records' => 0,
|
||||
'ad_groups_synced' => 0,
|
||||
'search_terms_synced' => 0,
|
||||
'negative_keywords_synced' => 0,
|
||||
'errors' => $errors
|
||||
];
|
||||
}
|
||||
|
||||
$all_time_map = [];
|
||||
$all_time_totals = [
|
||||
'cost' => 0.0,
|
||||
@@ -1325,7 +1438,7 @@ class Cron
|
||||
{
|
||||
foreach ( $campaigns_all_time as $cat )
|
||||
{
|
||||
$all_time_map[ $cat['campaign_id'] ] = $cat['roas_all_time'];
|
||||
$all_time_map[ (string) ( $cat['campaign_id'] ?? '' ) ] = (float) ( $cat['roas_all_time'] ?? 0 );
|
||||
$all_time_totals['cost'] += (float) ( $cat['cost_all_time'] ?? 0 );
|
||||
$all_time_totals['conversion_value'] += (float) ( $cat['conversion_value_all_time'] ?? 0 );
|
||||
}
|
||||
@@ -1393,18 +1506,18 @@ class Cron
|
||||
|
||||
if ( $mdb -> count( 'campaigns_history', [ 'AND' => [
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'date_add' => $today
|
||||
'date_add' => $as_of_date
|
||||
] ] ) )
|
||||
{
|
||||
$mdb -> update( 'campaigns_history', $history_data, [ 'AND' => [
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'date_add' => $today
|
||||
'date_add' => $as_of_date
|
||||
] ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$history_data['campaign_id'] = $db_campaign_id;
|
||||
$history_data['date_add'] = $today;
|
||||
$history_data['date_add'] = $as_of_date;
|
||||
$mdb -> insert( 'campaigns_history', $history_data );
|
||||
}
|
||||
|
||||
@@ -1457,26 +1570,37 @@ class Cron
|
||||
|
||||
if ( $mdb -> count( 'campaigns_history', [ 'AND' => [
|
||||
'campaign_id' => $db_account_campaign_id,
|
||||
'date_add' => $today
|
||||
'date_add' => $as_of_date
|
||||
] ] ) )
|
||||
{
|
||||
$mdb -> update( 'campaigns_history', $account_history_data, [ 'AND' => [
|
||||
'campaign_id' => $db_account_campaign_id,
|
||||
'date_add' => $today
|
||||
'date_add' => $as_of_date
|
||||
] ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$account_history_data['campaign_id'] = $db_account_campaign_id;
|
||||
$account_history_data['date_add'] = $today;
|
||||
$account_history_data['date_add'] = $as_of_date;
|
||||
$mdb -> insert( 'campaigns_history', $account_history_data );
|
||||
}
|
||||
|
||||
$processed++;
|
||||
|
||||
$ad_groups_sync = self::sync_campaign_ad_groups_for_client( $campaigns_db_map, $customer_id, $api, $today );
|
||||
$search_terms_sync = self::sync_campaign_search_terms_for_client( $campaigns_db_map, $ad_groups_sync['ad_group_map'], $customer_id, $api, $today );
|
||||
$negative_keywords_sync = self::sync_campaign_negative_keywords_for_client( $campaigns_db_map, $ad_groups_sync['ad_group_map'], $customer_id, $api, $today );
|
||||
if ( !$sync_details )
|
||||
{
|
||||
return [
|
||||
'processed_records' => $processed,
|
||||
'ad_groups_synced' => 0,
|
||||
'search_terms_synced' => 0,
|
||||
'negative_keywords_synced' => 0,
|
||||
'errors' => $errors
|
||||
];
|
||||
}
|
||||
|
||||
$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 );
|
||||
$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 );
|
||||
|
||||
$errors = array_merge( $errors, $ad_groups_sync['errors'], $search_terms_sync['errors'], $negative_keywords_sync['errors'] );
|
||||
|
||||
@@ -1856,17 +1980,104 @@ class Cron
|
||||
return [ 'count' => $count, 'errors' => [] ];
|
||||
}
|
||||
|
||||
static private function get_daily_cron_state( $state_key )
|
||||
static private function get_campaigns_window_state( $state_key, $anchor_date, $sync_dates )
|
||||
{
|
||||
$today = date( 'Y-m-d' );
|
||||
$anchor_date = date( 'Y-m-d', strtotime( $anchor_date ) );
|
||||
$sync_dates = array_values( array_unique( array_map( function( $item )
|
||||
{
|
||||
return date( 'Y-m-d', strtotime( $item ) );
|
||||
}, (array) $sync_dates ) ) );
|
||||
sort( $sync_dates );
|
||||
|
||||
if ( empty( $sync_dates ) )
|
||||
{
|
||||
$sync_dates = [ $anchor_date ];
|
||||
}
|
||||
|
||||
$expected_hash = md5( $anchor_date . '|' . implode( ',', $sync_dates ) );
|
||||
$state_raw = self::get_setting_value( $state_key, '' );
|
||||
$state = json_decode( (string) $state_raw, true );
|
||||
|
||||
if ( !is_array( $state ) || ( $state['window_hash'] ?? '' ) !== $expected_hash )
|
||||
{
|
||||
return [
|
||||
'anchor_date' => $anchor_date,
|
||||
'sync_dates' => $sync_dates,
|
||||
'current_date_index' => 0,
|
||||
'sync_date' => $sync_dates[0],
|
||||
'window_hash' => $expected_hash
|
||||
];
|
||||
}
|
||||
|
||||
$current_date_index = (int) ( $state['current_date_index'] ?? 0 );
|
||||
$current_date_index = max( 0, min( count( $sync_dates ) - 1, $current_date_index ) );
|
||||
|
||||
return [
|
||||
'anchor_date' => $anchor_date,
|
||||
'sync_dates' => $sync_dates,
|
||||
'current_date_index' => $current_date_index,
|
||||
'sync_date' => $sync_dates[ $current_date_index ],
|
||||
'window_hash' => $expected_hash
|
||||
];
|
||||
}
|
||||
|
||||
static private function save_campaigns_window_state( $state_key, $state )
|
||||
{
|
||||
$sync_dates = array_values( array_unique( array_map( function( $item )
|
||||
{
|
||||
return date( 'Y-m-d', strtotime( $item ) );
|
||||
}, (array) ( $state['sync_dates'] ?? [] ) ) ) );
|
||||
sort( $sync_dates );
|
||||
|
||||
if ( empty( $sync_dates ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$anchor_date = date( 'Y-m-d', strtotime( $state['anchor_date'] ?? end( $sync_dates ) ) );
|
||||
$current_date_index = max( 0, min( count( $sync_dates ) - 1, (int) ( $state['current_date_index'] ?? 0 ) ) );
|
||||
$payload = [
|
||||
'anchor_date' => $anchor_date,
|
||||
'sync_dates' => $sync_dates,
|
||||
'current_date_index' => $current_date_index,
|
||||
'sync_date' => $sync_dates[ $current_date_index ],
|
||||
'window_hash' => md5( $anchor_date . '|' . implode( ',', $sync_dates ) )
|
||||
];
|
||||
|
||||
self::set_setting_value( $state_key, json_encode( $payload, JSON_UNESCAPED_UNICODE ) );
|
||||
}
|
||||
|
||||
static private function advance_campaigns_window_state( $state )
|
||||
{
|
||||
$sync_dates = is_array( $state['sync_dates'] ?? null ) ? $state['sync_dates'] : [];
|
||||
$current_date_index = (int) ( $state['current_date_index'] ?? 0 );
|
||||
$last_index = count( $sync_dates ) - 1;
|
||||
|
||||
if ( $last_index < 0 )
|
||||
{
|
||||
return $state;
|
||||
}
|
||||
|
||||
if ( $current_date_index < $last_index )
|
||||
{
|
||||
$state['current_date_index'] = $current_date_index + 1;
|
||||
$state['sync_date'] = $sync_dates[ $state['current_date_index'] ];
|
||||
}
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
static private function get_daily_cron_state( $state_key, $state_date = null )
|
||||
{
|
||||
$state_date = $state_date ? date( 'Y-m-d', strtotime( $state_date ) ) : date( 'Y-m-d' );
|
||||
|
||||
$state_raw = self::get_setting_value( $state_key, '' );
|
||||
$state = json_decode( (string) $state_raw, true );
|
||||
|
||||
if ( !is_array( $state ) || ( $state['date'] ?? '' ) !== $today )
|
||||
if ( !is_array( $state ) || ( $state['date'] ?? '' ) !== $state_date )
|
||||
{
|
||||
return [
|
||||
'date' => $today,
|
||||
'date' => $state_date,
|
||||
'processed_ids' => []
|
||||
];
|
||||
}
|
||||
@@ -1885,15 +2096,16 @@ class Cron
|
||||
}
|
||||
|
||||
return [
|
||||
'date' => $today,
|
||||
'date' => $state_date,
|
||||
'processed_ids' => array_values( array_unique( $processed_ids ) )
|
||||
];
|
||||
}
|
||||
|
||||
static private function save_daily_cron_state( $state_key, $state )
|
||||
static private function save_daily_cron_state( $state_key, $state, $state_date = null )
|
||||
{
|
||||
$state_date = $state_date ? date( 'Y-m-d', strtotime( $state_date ) ) : date( 'Y-m-d' );
|
||||
$payload = [
|
||||
'date' => date( 'Y-m-d' ),
|
||||
'date' => $state_date,
|
||||
'processed_ids' => array_values( array_unique( array_map( 'intval', $state['processed_ids'] ?? [] ) ) )
|
||||
];
|
||||
|
||||
@@ -1924,6 +2136,42 @@ class Cron
|
||||
return 0;
|
||||
}
|
||||
|
||||
static private function get_conversion_window_days()
|
||||
{
|
||||
$request_value = (int) \S::get( 'conversion_window_days' );
|
||||
if ( $request_value > 0 )
|
||||
{
|
||||
return min( 90, $request_value );
|
||||
}
|
||||
|
||||
$setting_value = (int) self::get_setting_value( 'google_ads_conversion_window_days', 7 );
|
||||
if ( $setting_value <= 0 )
|
||||
{
|
||||
return 7;
|
||||
}
|
||||
|
||||
return min( 90, $setting_value );
|
||||
}
|
||||
|
||||
static private function build_backfill_dates( $end_date, $window_days )
|
||||
{
|
||||
$end_timestamp = strtotime( $end_date );
|
||||
if ( !$end_timestamp )
|
||||
{
|
||||
$end_timestamp = strtotime( date( 'Y-m-d' ) );
|
||||
}
|
||||
|
||||
$window_days = max( 1, min( 90, (int) $window_days ) );
|
||||
$dates = [];
|
||||
|
||||
for ( $i = $window_days - 1; $i >= 0; $i-- )
|
||||
{
|
||||
$dates[] = date( 'Y-m-d', strtotime( '-' . $i . ' days', $end_timestamp ) );
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
static private function get_setting_value( $setting_key, $default = null )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
@@ -465,100 +465,177 @@ class GoogleAdsApi
|
||||
. "FROM shopping_performance_view "
|
||||
. "WHERE segments.date = '" . $date . "'";
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql_with_ad_group );
|
||||
$fallback_without_ad_group = false;
|
||||
$gaql_pmax_asset_group = "SELECT "
|
||||
. "segments.date, "
|
||||
. "segments.product_item_id, "
|
||||
. "segments.product_title, "
|
||||
. "campaign.id, "
|
||||
. "campaign.name, "
|
||||
. "asset_group.id, "
|
||||
. "asset_group.name, "
|
||||
. "metrics.impressions, "
|
||||
. "metrics.clicks, "
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM asset_group_product_group_view "
|
||||
. "WHERE segments.date = '" . $date . "' "
|
||||
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'";
|
||||
|
||||
if ( $results === false )
|
||||
$gaql_pmax_campaign_level_fallback = "SELECT "
|
||||
. "segments.date, "
|
||||
. "segments.product_item_id, "
|
||||
. "segments.product_title, "
|
||||
. "campaign.id, "
|
||||
. "campaign.name, "
|
||||
. "campaign.advertising_channel_type, "
|
||||
. "metrics.impressions, "
|
||||
. "metrics.clicks, "
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM shopping_performance_view "
|
||||
. "WHERE segments.date = '" . $date . "'";
|
||||
|
||||
$results_with_ad_group = $this -> search_stream( $customer_id, $gaql_with_ad_group );
|
||||
$results_pmax_asset_group = $this -> search_stream( $customer_id, $gaql_pmax_asset_group );
|
||||
$results_pmax_campaign_fallback = [];
|
||||
|
||||
$had_success = false;
|
||||
|
||||
if ( is_array( $results_with_ad_group ) )
|
||||
{
|
||||
$gaql_without_ad_group = "SELECT "
|
||||
. "segments.date, "
|
||||
. "segments.product_item_id, "
|
||||
. "segments.product_title, "
|
||||
. "campaign.id, "
|
||||
. "campaign.name, "
|
||||
. "metrics.impressions, "
|
||||
. "metrics.clicks, "
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM shopping_performance_view "
|
||||
. "WHERE segments.date = '" . $date . "'";
|
||||
$had_success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
$results_with_ad_group = [];
|
||||
}
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql_without_ad_group );
|
||||
if ( $results === false )
|
||||
if ( is_array( $results_pmax_asset_group ) )
|
||||
{
|
||||
$had_success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
$results_pmax_asset_group = [];
|
||||
|
||||
// Fallback dla kont/API, gdzie asset_group_product_group_view moze nie byc dostepny.
|
||||
$tmp = $this -> search_stream( $customer_id, $gaql_pmax_campaign_level_fallback );
|
||||
if ( is_array( $tmp ) )
|
||||
{
|
||||
return false;
|
||||
$had_success = true;
|
||||
foreach ( $tmp as $row )
|
||||
{
|
||||
$channel = (string) ( $row['campaign']['advertisingChannelType'] ?? '' );
|
||||
if ( strtoupper( $channel ) === 'PERFORMANCE_MAX' )
|
||||
{
|
||||
$results_pmax_campaign_fallback[] = $row;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$fallback_without_ad_group = true;
|
||||
if ( !$had_success )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$products = [];
|
||||
|
||||
foreach ( $results as $row )
|
||||
$collect_rows = function( $rows, $group_kind ) use ( &$products )
|
||||
{
|
||||
$offer_id = trim( (string) ( $row['segments']['productItemId'] ?? '' ) );
|
||||
if ( $offer_id === '' )
|
||||
if ( !is_array( $rows ) )
|
||||
{
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
$campaign_id = (int) ( $row['campaign']['id'] ?? 0 );
|
||||
$campaign_name = trim( (string) ( $row['campaign']['name'] ?? '' ) );
|
||||
if ( $campaign_name === '' && $campaign_id > 0 )
|
||||
foreach ( $rows as $row )
|
||||
{
|
||||
$campaign_name = 'Kampania #' . $campaign_id;
|
||||
}
|
||||
|
||||
$ad_group_id = 0;
|
||||
$ad_group_name = 'PMax (bez grup reklam)';
|
||||
|
||||
if ( !$fallback_without_ad_group )
|
||||
{
|
||||
$ad_group_id = (int) ( $row['adGroup']['id'] ?? 0 );
|
||||
$ad_group_name = trim( (string) ( $row['adGroup']['name'] ?? '' ) );
|
||||
|
||||
if ( $ad_group_id > 0 && $ad_group_name === '' )
|
||||
$offer_id = trim( (string) ( $row['segments']['productItemId'] ?? '' ) );
|
||||
if ( $offer_id === '' )
|
||||
{
|
||||
$ad_group_name = 'Ad group #' . $ad_group_id;
|
||||
continue;
|
||||
}
|
||||
else if ( $ad_group_id <= 0 )
|
||||
|
||||
$campaign_id = (int) ( $row['campaign']['id'] ?? 0 );
|
||||
$campaign_name = trim( (string) ( $row['campaign']['name'] ?? '' ) );
|
||||
if ( $campaign_name === '' && $campaign_id > 0 )
|
||||
{
|
||||
$ad_group_name = 'PMax (bez grup reklam)';
|
||||
$campaign_name = 'Kampania #' . $campaign_id;
|
||||
}
|
||||
|
||||
$ad_group_id = 0;
|
||||
$ad_group_name = 'PMax (bez grup reklam)';
|
||||
|
||||
if ( $group_kind === 'ad_group' )
|
||||
{
|
||||
$ad_group_id = (int) ( $row['adGroup']['id'] ?? 0 );
|
||||
$ad_group_name = trim( (string) ( $row['adGroup']['name'] ?? '' ) );
|
||||
|
||||
if ( $ad_group_id > 0 && $ad_group_name === '' )
|
||||
{
|
||||
$ad_group_name = 'Ad group #' . $ad_group_id;
|
||||
}
|
||||
else if ( $ad_group_id <= 0 )
|
||||
{
|
||||
$ad_group_name = 'PMax (bez grup reklam)';
|
||||
}
|
||||
}
|
||||
else if ( $group_kind === 'asset_group' )
|
||||
{
|
||||
$ad_group_id = (int) ( $row['assetGroup']['id'] ?? 0 );
|
||||
$ad_group_name = trim( (string) ( $row['assetGroup']['name'] ?? '' ) );
|
||||
|
||||
if ( $ad_group_id > 0 && $ad_group_name === '' )
|
||||
{
|
||||
$ad_group_name = 'Asset group #' . $ad_group_id;
|
||||
}
|
||||
else if ( $ad_group_id <= 0 )
|
||||
{
|
||||
$ad_group_name = 'PMax (bez grup plikow)';
|
||||
}
|
||||
}
|
||||
|
||||
$scope_key = $offer_id . '|' . $campaign_id . '|' . $ad_group_id;
|
||||
|
||||
if ( !isset( $products[ $scope_key ] ) )
|
||||
{
|
||||
$products[ $scope_key ] = [
|
||||
'OfferId' => $offer_id,
|
||||
'ProductTitle' => (string) ( $row['segments']['productTitle'] ?? $offer_id ),
|
||||
'CampaignId' => $campaign_id,
|
||||
'CampaignName' => $campaign_name,
|
||||
'AdGroupId' => $ad_group_id,
|
||||
'AdGroupName' => $ad_group_name,
|
||||
'Impressions' => 0,
|
||||
'Clicks' => 0,
|
||||
'Cost' => 0.0,
|
||||
'Conversions' => 0.0,
|
||||
'ConversionValue' => 0.0
|
||||
];
|
||||
}
|
||||
|
||||
$products[ $scope_key ]['Impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
|
||||
$products[ $scope_key ]['Clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
|
||||
$products[ $scope_key ]['Cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
|
||||
$products[ $scope_key ]['Conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
|
||||
$products[ $scope_key ]['ConversionValue'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
|
||||
}
|
||||
};
|
||||
|
||||
$scope_key = $offer_id . '|' . $campaign_id . '|' . $ad_group_id;
|
||||
|
||||
if ( !isset( $products[ $scope_key ] ) )
|
||||
{
|
||||
$products[ $scope_key ] = [
|
||||
'OfferId' => $offer_id,
|
||||
'ProductTitle' => (string) ( $row['segments']['productTitle'] ?? $offer_id ),
|
||||
'CampaignId' => $campaign_id,
|
||||
'CampaignName' => $campaign_name,
|
||||
'AdGroupId' => $ad_group_id,
|
||||
'AdGroupName' => $ad_group_name,
|
||||
'Impressions' => 0,
|
||||
'Clicks' => 0,
|
||||
'Cost' => 0.0,
|
||||
'Conversions' => 0.0,
|
||||
'ConversionValue' => 0.0
|
||||
];
|
||||
}
|
||||
|
||||
$products[ $scope_key ]['Impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
|
||||
$products[ $scope_key ]['Clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
|
||||
$products[ $scope_key ]['Cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
|
||||
$products[ $scope_key ]['Conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
|
||||
$products[ $scope_key ]['ConversionValue'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
|
||||
}
|
||||
$collect_rows( $results_with_ad_group, 'ad_group' );
|
||||
$collect_rows( $results_pmax_asset_group, 'asset_group' );
|
||||
$collect_rows( $results_pmax_campaign_fallback, 'campaign' );
|
||||
|
||||
return array_values( $products );
|
||||
}
|
||||
|
||||
public function get_campaigns_30_days( $customer_id )
|
||||
public function get_campaigns_30_days( $customer_id, $as_of_date = null )
|
||||
{
|
||||
$as_of_date = $as_of_date ? date( 'Y-m-d', strtotime( $as_of_date ) ) : date( 'Y-m-d' );
|
||||
$date_from = date( 'Y-m-d', strtotime( '-29 days', strtotime( $as_of_date ) ) );
|
||||
|
||||
$gaql = "SELECT "
|
||||
. "campaign.id, "
|
||||
. "campaign.name, "
|
||||
@@ -570,7 +647,8 @@ class GoogleAdsApi
|
||||
. "metrics.conversions_value "
|
||||
. "FROM campaign "
|
||||
. "WHERE campaign.status = 'ENABLED' "
|
||||
. "AND segments.date DURING LAST_30_DAYS";
|
||||
. "AND segments.date >= '" . $date_from . "' "
|
||||
. "AND segments.date <= '" . $as_of_date . "'";
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql );
|
||||
if ( $results === false ) return false;
|
||||
@@ -619,16 +697,34 @@ class GoogleAdsApi
|
||||
|
||||
// --- Kampanie: dane all-time ---
|
||||
|
||||
public function get_campaigns_all_time( $customer_id )
|
||||
public function get_campaigns_all_time( $customer_id, $as_of_date = null )
|
||||
{
|
||||
$gaql = "SELECT "
|
||||
. "campaign.id, "
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM campaign "
|
||||
. "WHERE campaign.status = 'ENABLED'";
|
||||
$as_of_date = $as_of_date ? date( 'Y-m-d', strtotime( $as_of_date ) ) : null;
|
||||
|
||||
$gaql_base = "SELECT "
|
||||
. "campaign.id, "
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM campaign "
|
||||
. "WHERE campaign.status = 'ENABLED'";
|
||||
|
||||
$results = false;
|
||||
if ( $as_of_date )
|
||||
{
|
||||
$gaql_with_date = $gaql_base . " AND segments.date <= '" . $as_of_date . "'";
|
||||
$results = $this -> search_stream( $customer_id, $gaql_with_date );
|
||||
|
||||
// Fallback do starego sposobu, gdy filtr daty nie jest akceptowany na danym koncie.
|
||||
if ( $results === false )
|
||||
{
|
||||
$results = $this -> search_stream( $customer_id, $gaql_base );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$results = $this -> search_stream( $customer_id, $gaql_base );
|
||||
}
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql );
|
||||
if ( $results === false ) return false;
|
||||
|
||||
$campaigns = [];
|
||||
@@ -637,18 +733,33 @@ class GoogleAdsApi
|
||||
$cid = $row['campaign']['id'] ?? null;
|
||||
if ( !$cid ) continue;
|
||||
|
||||
$cost = (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
|
||||
$value = (float) ( $row['metrics']['conversionsValue'] ?? 0 );
|
||||
if ( !isset( $campaigns[ $cid ] ) )
|
||||
{
|
||||
$campaigns[ $cid ] = [
|
||||
'campaign_id' => $cid,
|
||||
'cost_micros_total' => 0.0,
|
||||
'conversion_value_total' => 0.0
|
||||
];
|
||||
}
|
||||
|
||||
$campaigns[] = [
|
||||
'campaign_id' => $cid,
|
||||
'cost_all_time' => $cost,
|
||||
'conversion_value_all_time' => $value,
|
||||
'roas_all_time' => ( $cost > 0 ) ? round( ( $value / $cost ) * 100, 2 ) : 0,
|
||||
$campaigns[ $cid ]['cost_micros_total'] += (float) ( $row['metrics']['costMicros'] ?? 0 );
|
||||
$campaigns[ $cid ]['conversion_value_total'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
|
||||
}
|
||||
|
||||
foreach ( $campaigns as &$campaign )
|
||||
{
|
||||
$cost = $campaign['cost_micros_total'] / 1000000;
|
||||
$value = $campaign['conversion_value_total'];
|
||||
|
||||
$campaign = [
|
||||
'campaign_id' => $campaign['campaign_id'],
|
||||
'cost_all_time' => $cost,
|
||||
'conversion_value_all_time' => $value,
|
||||
'roas_all_time' => ( $cost > 0 ) ? round( ( $value / $cost ) * 100, 2 ) : 0
|
||||
];
|
||||
}
|
||||
|
||||
return $campaigns;
|
||||
return array_values( $campaigns );
|
||||
}
|
||||
|
||||
public function get_ad_groups_30_days( $customer_id )
|
||||
|
||||
@@ -30,6 +30,13 @@
|
||||
<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">
|
||||
<summary>Widocznosc kolumn</summary>
|
||||
<div class="products-columns-list" id="products_columns_list"></div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Akcje bulk -->
|
||||
@@ -76,9 +83,63 @@
|
||||
$openai_enabled = \services\GoogleAdsApi::get_setting( 'openai_enabled' ) !== '0';
|
||||
$claude_enabled = \services\GoogleAdsApi::get_setting( 'claude_enabled' ) !== '0';
|
||||
?>
|
||||
<style>
|
||||
.products-page .products-filters .filter-group.filter-group-columns {
|
||||
min-width: 240px;
|
||||
}
|
||||
.products-columns-control {
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 6px;
|
||||
background: #FFFFFF;
|
||||
overflow: hidden;
|
||||
}
|
||||
.products-columns-control summary {
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
list-style: none;
|
||||
}
|
||||
.products-columns-control summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.products-columns-control summary::after {
|
||||
content: '\25BC';
|
||||
float: right;
|
||||
font-size: 10px;
|
||||
color: #64748B;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.products-columns-control[open] summary::after {
|
||||
content: '\25B2';
|
||||
}
|
||||
.products-columns-list {
|
||||
border-top: 1px solid #EEF2F7;
|
||||
padding: 8px 10px;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.products-columns-list .products-col-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.products-columns-list .products-col-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.products-columns-list .products-col-item input[type=checkbox] {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
var AI_OPENAI_ENABLED = <?= $openai_enabled ? 'true' : 'false'; ?>;
|
||||
var AI_CLAUDE_ENABLED = <?= $claude_enabled ? 'true' : 'false'; ?>;
|
||||
var PRODUCTS_COLUMNS_STORAGE_KEY = 'products.columns.visibility';
|
||||
var PRODUCTS_LOCKED_COLUMNS = [ 0, 19 ];
|
||||
|
||||
function show_toast( message, type )
|
||||
{
|
||||
@@ -123,6 +184,151 @@ function loadGoogleCategories( callback )
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function products_storage_set( key, value )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( value === null || typeof value === 'undefined' )
|
||||
{
|
||||
localStorage.removeItem( key );
|
||||
}
|
||||
else
|
||||
{
|
||||
localStorage.setItem( key, String( value ) );
|
||||
}
|
||||
}
|
||||
catch ( e ) {}
|
||||
}
|
||||
|
||||
function products_storage_get( key )
|
||||
{
|
||||
try
|
||||
{
|
||||
return localStorage.getItem( key ) || '';
|
||||
}
|
||||
catch ( e )
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function products_is_locked_column( idx )
|
||||
{
|
||||
return PRODUCTS_LOCKED_COLUMNS.indexOf( Number( idx ) ) !== -1;
|
||||
}
|
||||
|
||||
function products_get_saved_columns_visibility( columns_count )
|
||||
{
|
||||
var raw = products_storage_get( PRODUCTS_COLUMNS_STORAGE_KEY );
|
||||
if ( !raw )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var saved = JSON.parse( raw );
|
||||
if ( !Array.isArray( saved ) || saved.length !== columns_count )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
catch ( e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function products_save_columns_visibility( table_instance )
|
||||
{
|
||||
if ( !table_instance || !table_instance.columns )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var columns_count = table_instance.columns().count();
|
||||
var visible_map = [];
|
||||
var i;
|
||||
|
||||
for ( i = 0; i < columns_count; i++ )
|
||||
{
|
||||
visible_map.push( table_instance.column( i ).visible() );
|
||||
}
|
||||
|
||||
products_storage_set( PRODUCTS_COLUMNS_STORAGE_KEY, JSON.stringify( visible_map ) );
|
||||
}
|
||||
|
||||
function products_apply_saved_columns_visibility( table_instance )
|
||||
{
|
||||
if ( !table_instance || !table_instance.columns )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var columns_count = table_instance.columns().count();
|
||||
var saved_visibility = products_get_saved_columns_visibility( columns_count );
|
||||
var i;
|
||||
|
||||
if ( !saved_visibility )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for ( i = 0; i < columns_count; i++ )
|
||||
{
|
||||
if ( products_is_locked_column( i ) )
|
||||
{
|
||||
table_instance.column( i ).visible( true, false );
|
||||
continue;
|
||||
}
|
||||
|
||||
table_instance.column( i ).visible( !!saved_visibility[i], false );
|
||||
}
|
||||
|
||||
table_instance.columns.adjust().draw( false );
|
||||
}
|
||||
|
||||
function products_render_columns_picker( table_instance )
|
||||
{
|
||||
var $list = $( '#products_columns_list' );
|
||||
var columns_count = ( table_instance && table_instance.columns ) ? table_instance.columns().count() : 0;
|
||||
var i;
|
||||
|
||||
if ( !$list.length )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$list.empty();
|
||||
|
||||
if ( !columns_count )
|
||||
{
|
||||
$list.append( '<div class="products-col-item">Brak kolumn.</div>' );
|
||||
return;
|
||||
}
|
||||
|
||||
for ( i = 0; i < columns_count; i++ )
|
||||
{
|
||||
if ( products_is_locked_column( i ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var id = 'products-col-toggle-' + i;
|
||||
var th = $( '#products thead th' ).eq( i );
|
||||
var title = $.trim( th.find( '.dt-column-title' ).first().text() || th.text() ) || ( 'Kolumna ' + i );
|
||||
var checked = table_instance.column( i ).visible() ? ' checked' : '';
|
||||
|
||||
$list.append(
|
||||
'<label class="products-col-item" for="' + id + '">' +
|
||||
'<input type="checkbox" class="products-col-toggle" id="' + id + '" data-col-index="' + i + '"' + checked + '>' +
|
||||
'<span>' + title + '</span>' +
|
||||
'</label>'
|
||||
);
|
||||
}
|
||||
}
|
||||
$( function()
|
||||
{
|
||||
var products_table = new DataTable( '#products', {
|
||||
@@ -181,6 +387,9 @@ $( function()
|
||||
}
|
||||
});
|
||||
|
||||
products_apply_saved_columns_visibility( products_table );
|
||||
products_render_columns_picker( products_table );
|
||||
|
||||
function reload_products_table()
|
||||
{
|
||||
products_table.ajax.reload( null, false );
|
||||
@@ -648,6 +857,22 @@ $( function()
|
||||
updateSelectedCount();
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'change', '.products-col-toggle', function()
|
||||
{
|
||||
var col_index = Number( $( this ).data( 'col-index' ) );
|
||||
var is_visible = $( this ).is( ':checked' );
|
||||
|
||||
if ( !products_table || Number.isNaN( col_index ) || products_is_locked_column( col_index ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
products_table.column( col_index ).visible( is_visible, false );
|
||||
products_table.columns.adjust().draw( false );
|
||||
products_save_columns_visibility( products_table );
|
||||
products_render_columns_picker( products_table );
|
||||
});
|
||||
|
||||
// Usuwanie zaznaczonych produktów
|
||||
$( 'body' ).on( 'click', '#delete-selected-products', function()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user