feat: add logs page with filtering and data table

- Implemented a new logs page with filters for level, source, and date range.
- Added a data table to display logs with pagination and sorting capabilities.
- Created backend functionality to fetch logs data based on filters.
- Introduced a new Logs class for handling log data operations.
- Added a new database migration for the logs table.
- Enhanced UI with custom checkbox styles for better user experience.
- Updated navigation to include a link to the logs page.
This commit is contained in:
2026-02-21 13:05:59 +01:00
parent b54a9a71b1
commit bc75eab439
17 changed files with 1107 additions and 571 deletions

View File

@@ -21,7 +21,8 @@
"Bash(python3:*)", "Bash(python3:*)",
"Bash(py --version)", "Bash(py --version)",
"Bash(where:*)", "Bash(where:*)",
"Bash(python:*)" "Bash(python:*)",
"Bash(ls -la \"c:\\\\visual studio code\\\\projekty\\\\adsPRO\\\\docs\"\" 2>/dev/null || echo \"docs dir not found \")"
] ]
} }
} }

View File

@@ -20,6 +20,54 @@
"modified": false "modified": false
}, },
"autoload": { "autoload": {
"class.Cache.php": {
"type": "-",
"size": 1006,
"lmtime": 0,
"modified": false
},
"class.Chunk.php": {
"type": "-",
"size": 7304,
"lmtime": 0,
"modified": false
},
"class.Cron.php": {
"type": "-",
"size": 8901,
"lmtime": 0,
"modified": false
},
"class.DbModel.php": {
"type": "-",
"size": 1392,
"lmtime": 0,
"modified": false
},
"class.Excel.php": {
"type": "-",
"size": 4319,
"lmtime": 0,
"modified": false
},
"class.Html.php": {
"type": "-",
"size": 2105,
"lmtime": 0,
"modified": false
},
"class.S.php": {
"type": "-",
"size": 8418,
"lmtime": 0,
"modified": false
},
"class.Tpl.php": {
"type": "-",
"size": 1839,
"lmtime": 0,
"modified": false
},
"controls": { "controls": {
"class.Allegro.php": { "class.Allegro.php": {
"type": "-", "type": "-",
@@ -29,14 +77,20 @@
}, },
"class.Api.php": { "class.Api.php": {
"type": "-", "type": "-",
"size": 19358, "size": 19626,
"lmtime": 1744498273470, "lmtime": 1744498273470,
"modified": true "modified": true
}, },
"class.CampaignAlerts.php": {
"type": "-",
"size": 2232,
"lmtime": 0,
"modified": false
},
"class.Campaigns.php": { "class.Campaigns.php": {
"type": "-", "type": "-",
"size": 5378, "size": 6153,
"lmtime": 1771486264591, "lmtime": 1771626580811,
"modified": false "modified": false
}, },
"class.CampaignTerms.php": { "class.CampaignTerms.php": {
@@ -47,21 +101,27 @@
}, },
"class.Clients.php": { "class.Clients.php": {
"type": "-", "type": "-",
"size": 3143, "size": 11854,
"lmtime": 1771494823776, "lmtime": 1771619242656,
"modified": false "modified": false
}, },
"class.Cron.php": { "class.Cron.php": {
"type": "-", "type": "-",
"size": 116612, "size": 198900,
"lmtime": 1771511676177, "lmtime": 1771619004019,
"modified": false
},
"class.FacebookAds.php": {
"type": "-",
"size": 4162,
"lmtime": 1771619366367,
"modified": false "modified": false
}, },
"class.Products.php": { "class.Products.php": {
"type": "-", "type": "-",
"size": 39363, "size": 44261,
"lmtime": 1771440055487, "lmtime": 1771440055487,
"modified": false "modified": true
}, },
"class.Site.php": { "class.Site.php": {
"type": "-", "type": "-",
@@ -71,8 +131,8 @@
}, },
"class.Users.php": { "class.Users.php": {
"type": "-", "type": "-",
"size": 13549, "size": 19292,
"lmtime": 1771493809548, "lmtime": 1771617570626,
"modified": false "modified": false
}, },
"class.XmlFiles.php": { "class.XmlFiles.php": {
@@ -83,10 +143,16 @@
} }
}, },
"factory": { "factory": {
"class.CampaignAlerts.php": {
"type": "-",
"size": 3222,
"lmtime": 0,
"modified": false
},
"class.Campaigns.php": { "class.Campaigns.php": {
"type": "-", "type": "-",
"size": 10980, "size": 11156,
"lmtime": 1771486281723, "lmtime": 1771626553779,
"modified": false "modified": false
}, },
"class.Clients.php": { "class.Clients.php": {
@@ -101,9 +167,15 @@
"lmtime": 0, "lmtime": 0,
"modified": false "modified": false
}, },
"class.FacebookAds.php": {
"type": "-",
"size": 29620,
"lmtime": 1771619061605,
"modified": false
},
"class.Products.php": { "class.Products.php": {
"type": "-", "type": "-",
"size": 27192, "size": 32530,
"lmtime": 1771170224109, "lmtime": 1771170224109,
"modified": true "modified": true
}, },
@@ -127,11 +199,17 @@
"lmtime": 1771198088093, "lmtime": 1771198088093,
"modified": true "modified": true
}, },
"class.FacebookAdsApi.php": {
"type": "-",
"size": 11724,
"lmtime": 1771619153702,
"modified": false
},
"class.GoogleAdsApi.php": { "class.GoogleAdsApi.php": {
"type": "-", "type": "-",
"size": 99181, "size": 114140,
"lmtime": 1771444236566, "lmtime": 1771444236566,
"modified": false "modified": true
}, },
"class.OpenAiApi.php": { "class.OpenAiApi.php": {
"type": "-", "type": "-",
@@ -139,13 +217,39 @@
"lmtime": 1771171891986, "lmtime": 1771171891986,
"modified": true "modified": true
} }
},
"view": {
"class.Clients.php": {
"type": "-",
"size": 192,
"lmtime": 0,
"modified": false
},
"class.Cron.php": {
"type": "-",
"size": 164,
"lmtime": 0,
"modified": false
},
"class.Site.php": {
"type": "-",
"size": 649,
"lmtime": 0,
"modified": false
},
"class.Users.php": {
"type": "-",
"size": 415,
"lmtime": 0,
"modified": false
}
} }
}, },
".claude": { ".claude": {
"settings.local.json": { "settings.local.json": {
"type": "-", "type": "-",
"size": 549, "size": 730,
"lmtime": 1771493868314, "lmtime": 1771617115553,
"modified": false "modified": false
} }
}, },
@@ -157,9 +261,9 @@
}, },
"config.php": { "config.php": {
"type": "-", "type": "-",
"size": 624, "size": 921,
"lmtime": 1771497460705, "lmtime": 1771497460705,
"modified": false "modified": true
}, },
"cron.php": { "cron.php": {
"type": "-", "type": "-",
@@ -405,17 +509,77 @@
"lmtime": 1771489966129, "lmtime": 1771489966129,
"modified": false "modified": false
}, },
"demo_data.sql": {
"type": "-",
"size": 22351,
"lmtime": 0,
"modified": true
},
"012_cron_sync_status.sql": { "012_cron_sync_status.sql": {
"type": "-", "type": "-",
"size": 1830, "size": 1830,
"lmtime": 1771493459924, "lmtime": 1771493459924,
"modified": false "modified": false
},
"013_campaign_alerts.sql": {
"type": "-",
"size": 880,
"lmtime": 0,
"modified": false
},
"014_campaign_search_terms_history.sql": {
"type": "-",
"size": 1343,
"lmtime": 0,
"modified": false
},
"015_campaign_alerts_unseen.sql": {
"type": "-",
"size": 337,
"lmtime": 0,
"modified": false
},
"016_products_model_unification.sql": {
"type": "-",
"size": 5857,
"lmtime": 0,
"modified": false
},
"017_drop_products_data.sql": {
"type": "-",
"size": 161,
"lmtime": 0,
"modified": false
},
"018_products_merchant_url_flags.sql": {
"type": "-",
"size": 1104,
"lmtime": 0,
"modified": false
},
"019_campaign_alerts_product_id.sql": {
"type": "-",
"size": 813,
"lmtime": 0,
"modified": false
},
"020_facebook_ads_base.sql": {
"type": "-",
"size": 6195,
"lmtime": 0,
"modified": false
},
"021_facebook_ads_conversion_metrics.sql": {
"type": "-",
"size": 2479,
"lmtime": 0,
"modified": false
},
"demo_data.sql": {
"type": "-",
"size": 21146,
"lmtime": 0,
"modified": true
},
"022_facebook_ads_roas_all_time.sql": {
"type": "-",
"size": 478,
"lmtime": 1771616256503,
"modified": false
} }
}, },
"robots.txt": { "robots.txt": {
@@ -424,6 +588,36 @@
"lmtime": 1744488227849, "lmtime": 1744488227849,
"modified": false "modified": false
}, },
"temp_fb_authentication.html": {
"type": "-",
"size": 1167861,
"lmtime": 0,
"modified": false
},
"temp_fb_authorization.html": {
"type": "-",
"size": 1182404,
"lmtime": 0,
"modified": false
},
"temp_fb_get_started.html": {
"type": "-",
"size": 1160708,
"lmtime": 0,
"modified": false
},
"temp_fb_insights_async.html": {
"type": "-",
"size": 1190062,
"lmtime": 0,
"modified": false
},
"temp_fb_system_users.html": {
"type": "-",
"size": 1172574,
"lmtime": 0,
"modified": false
},
"templates": { "templates": {
"products": { "products": {
"main_view.php": { "main_view.php": {
@@ -462,8 +656,8 @@
"campaigns": { "campaigns": {
"main_view.php": { "main_view.php": {
"type": "-", "type": "-",
"size": 17073, "size": 20532,
"lmtime": 1771497969444, "lmtime": 1771626632932,
"modified": false "modified": false
} }
}, },
@@ -496,6 +690,14 @@
"lmtime": 1771494851645, "lmtime": 1771494851645,
"modified": false "modified": false
} }
},
"facebook_ads": {
"main_view.php": {
"type": "-",
"size": 10785,
"lmtime": 1771619352316,
"modified": false
}
} }
}, },
"tmp": {}, "tmp": {},

View File

@@ -2,12 +2,15 @@
namespace controls; namespace controls;
class Cron class Cron
{ {
static private $current_cron_action = 'cron';
// Uniwersalny CRON pipeline. // Uniwersalny CRON pipeline.
// Jedno wywolanie = jeden klient + jeden dzien: kampanie -> produkty. // Jedno wywolanie = jeden klient + jeden dzien: kampanie -> produkty.
static public function cron_universal() static public function cron_universal()
{ {
global $mdb; global $mdb;
self::$current_cron_action = __FUNCTION__;
self::touch_cron_invocation( __FUNCTION__ ); self::touch_cron_invocation( __FUNCTION__ );
$clients_not_deleted_sql = self::sql_clients_not_deleted(); $clients_not_deleted_sql = self::sql_clients_not_deleted();
@@ -484,6 +487,7 @@ class Cron
static public function cron_products_urls() static public function cron_products_urls()
{ {
global $mdb, $settings; global $mdb, $settings;
self::$current_cron_action = __FUNCTION__;
self::touch_cron_invocation( __FUNCTION__ ); self::touch_cron_invocation( __FUNCTION__ );
$api = new \services\GoogleAdsApi(); $api = new \services\GoogleAdsApi();
@@ -1091,24 +1095,6 @@ class Cron
$campaigns_by_db_id[ $db_campaign_id ] = $campaign_data; $campaigns_by_db_id[ $db_campaign_id ] = $campaign_data;
} }
$existing_campaign_histories = $mdb -> query(
'SELECT ch.campaign_id
FROM campaigns_history AS ch
INNER JOIN campaigns AS c ON c.id = ch.campaign_id
WHERE c.client_id = :client_id
AND ch.date_add = :date_add',
[
':client_id' => $client_id,
':date_add' => $date
]
) -> fetchAll( \PDO::FETCH_COLUMN );
$campaign_history_exists = [];
foreach ( (array) $existing_campaign_histories as $history_campaign_id )
{
$campaign_history_exists[ (int) $history_campaign_id ] = true;
}
$existing_ad_groups_rows = $mdb -> query( $existing_ad_groups_rows = $mdb -> query(
'SELECT ag.id, ag.campaign_id, ag.ad_group_id, ag.ad_group_name 'SELECT ag.id, ag.campaign_id, ag.ad_group_id, ag.ad_group_name
FROM campaign_ad_groups AS ag FROM campaign_ad_groups AS ag
@@ -1166,7 +1152,7 @@ class Cron
]; ];
} }
$resolve_scope_ids = function( $campaign_external_id, $campaign_name, $ad_group_external_id, $ad_group_name ) use ( &$campaigns_by_external_id, &$campaigns_by_db_id, &$campaign_history_exists, &$ad_groups_by_scope, $client_id, $date, $mdb ) $resolve_scope_ids = function( $campaign_external_id, $campaign_name, $ad_group_external_id, $ad_group_name ) use ( &$campaigns_by_external_id, &$campaigns_by_db_id, &$ad_groups_by_scope, $client_id, $date, $mdb )
{ {
$campaign_external_id = (int) $campaign_external_id; $campaign_external_id = (int) $campaign_external_id;
$campaign_name = trim( (string) $campaign_name ); $campaign_name = trim( (string) $campaign_name );
@@ -1213,22 +1199,6 @@ class Cron
$db_campaign_id = (int) ( $campaign_data['id'] ?? 0 ); $db_campaign_id = (int) ( $campaign_data['id'] ?? 0 );
if ( $db_campaign_id > 0 && !isset( $campaign_history_exists[ $db_campaign_id ] ) )
{
$mdb -> insert( 'campaigns_history', [
'campaign_id' => $db_campaign_id,
'roas_30_days' => 0,
'roas_all_time' => 0,
'budget' => 0,
'money_spent' => 0,
'conversion_value' => 0,
'bidding_strategy' => '',
'date_add' => $date
] );
$campaign_history_exists[ $db_campaign_id ] = true;
}
if ( $db_campaign_id <= 0 ) if ( $db_campaign_id <= 0 )
{ {
return [ 'campaign_id' => 0, 'ad_group_id' => 0 ]; return [ 'campaign_id' => 0, 'ad_group_id' => 0 ];
@@ -1546,23 +1516,6 @@ class Cron
$db_campaign_id = (int) $mdb -> id(); $db_campaign_id = (int) $mdb -> id();
if ( $db_campaign_id > 0 && $date_sync )
{
if ( !$mdb -> count( 'campaigns_history', [ 'AND' => [ 'campaign_id' => $db_campaign_id, 'date_add' => $date_sync ] ] ) )
{
$mdb -> insert( 'campaigns_history', [
'campaign_id' => $db_campaign_id,
'roas_30_days' => 0,
'roas_all_time' => 0,
'budget' => 0,
'money_spent' => 0,
'conversion_value' => 0,
'bidding_strategy' => '',
'date_add' => $date_sync
] );
}
}
return $db_campaign_id; return $db_campaign_id;
} }
@@ -1961,211 +1914,11 @@ class Cron
} }
} }
// ARCHIWUM: stara wersja cron_campaigns pozostawiona do porownan i ewentualnego rollbacku.
static public function cron_campaigns_archive()
{
global $mdb, $settings;
self::touch_cron_invocation( __FUNCTION__ );
$api = new \services\GoogleAdsApi();
if ( !$api -> is_configured() )
{
echo json_encode( [ 'result' => 'Google Ads API nie jest skonfigurowane. Uzupelnij dane w Ustawieniach.' ] );
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 )
{
$client = $mdb -> get( 'clients', '*', [ 'AND' => [
'id' => $client_id,
'google_ads_customer_id[!]' => null,
'deleted' => 0
] ] );
if ( !$client )
{
echo json_encode( [ 'result' => 'Nie znaleziono klienta z poprawnym Google Ads Customer ID.', 'client_id' => $client_id ] );
exit;
}
$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 ),
'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;
}
self::cleanup_old_sync_rows( 30 );
$client_ids = $mdb -> query( "SELECT id FROM clients WHERE deleted = 0 AND google_ads_customer_id IS NOT NULL AND google_ads_customer_id <> '' ORDER BY id ASC" ) -> fetchAll( \PDO::FETCH_COLUMN );
$client_ids = array_values( array_unique( array_map( 'intval', $client_ids ) ) );
if ( empty( $client_ids ) )
{
echo json_encode( [ 'result' => 'Brak klientow z ustawionym Google Ads Customer ID.' ] );
exit;
}
$clients_map = [];
$clients = $mdb -> select( 'clients', '*', [
'AND' => [
'google_ads_customer_id[!]' => null,
'deleted' => 0
],
'ORDER' => [ 'id' => 'ASC' ]
] );
foreach ( $clients as $c )
{
$clients_map[ (int) $c['id'] ] = $c;
}
self::ensure_sync_rows( 'campaigns', $sync_dates, $client_ids );
$active_client_id = self::get_active_client( 'campaigns' );
if ( !$active_client_id )
{
echo json_encode( [
'result' => 'Wszyscy klienci kampanii zostali juz przetworzeni dla calego okna dat.',
'date' => $sync_date,
'active_date' => $sync_date,
'conversion_window_days' => $conversion_window_days,
'dates_synced' => $sync_dates,
'processed_clients' => count( $client_ids ),
'total_clients' => count( $client_ids )
] );
exit;
}
$dates_per_run_default = (int) ( $settings['cron_campaigns_clients_per_run'] ?? 2 );
if ( $dates_per_run_default <= 0 )
{
$dates_per_run_default = 2;
}
$dates_per_run = (int) \S::get( 'clients_per_run' );
if ( $dates_per_run <= 0 )
{
$dates_per_run = $dates_per_run_default;
}
$dates_per_run = min( 20, $dates_per_run );
$dates_batch = self::get_pending_dates_for_client( 'campaigns', $active_client_id, 'pending', $dates_per_run );
if ( empty( $dates_batch ) )
{
echo json_encode( [
'result' => 'Wszystkie daty klienta przetworzone. Kolejne wywolanie przejdzie do nastepnego klienta.',
'date' => $sync_date,
'active_client_id' => $active_client_id,
'conversion_window_days' => $conversion_window_days,
'dates_synced' => $sync_dates,
'processed_clients' => count( $client_ids ),
'total_clients' => count( $client_ids )
] );
exit;
}
$selected_client = $clients_map[ $active_client_id ] ?? null;
if ( !$selected_client )
{
echo json_encode( [
'result' => 'Nie udalo sie znalezc klienta do synchronizacji kampanii. ID: ' . $active_client_id,
'active_client_id' => $active_client_id,
'errors' => [ 'Klient ID ' . $active_client_id . ' nie znaleziony w clients_map.' ]
] );
exit;
}
$dates_processed_in_call = [];
$errors = [];
$processed_records_total = 0;
$ad_groups_synced_total = 0;
$search_terms_synced_total = 0;
$keywords_synced_total = 0;
$negative_keywords_synced_total = 0;
$alerts_synced_total = 0;
foreach ( $dates_batch as $active_date )
{
$sync_details = ( $active_date === $sync_date );
$sync = self::sync_campaigns_for_client( $selected_client, $api, $active_date, $sync_details );
$processed_records_total += (int) ( $sync['processed_records'] ?? 0 );
$ad_groups_synced_total += (int) ( $sync['ad_groups_synced'] ?? 0 );
$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'] ) )
{
$errors = array_merge( $errors, (array) $sync['errors'] );
$error_msg = implode( '; ', (array) $sync['errors'] );
}
self::mark_sync_phase( 'campaigns', $active_date, $active_client_id, 'done', $error_msg );
$dates_processed_in_call[] = $active_date;
}
$done_count = (int) $mdb -> query(
"SELECT COUNT(*) FROM cron_sync_status WHERE pipeline = 'campaigns' AND client_id = :client_id AND phase = 'done'
AND sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)",
[ ':client_id' => $active_client_id ]
) -> fetchColumn();
$total_count = (int) $mdb -> query(
"SELECT COUNT(*) FROM cron_sync_status WHERE pipeline = 'campaigns' AND client_id = :client_id
AND sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)",
[ ':client_id' => $active_client_id ]
) -> fetchColumn();
$remaining_dates = max( 0, $total_count - $done_count );
$estimated_calls_remaining = (int) ceil( $remaining_dates / max( 1, $dates_per_run ) );
echo json_encode( [
'result' => empty( $errors ) ? 'Synchronizacja kampanii zakonczona.' : 'Synchronizacja kampanii zakonczona z bledami.',
'active_client_id' => $active_client_id,
'dates_per_run' => $dates_per_run,
'dates_processed_in_call' => count( $dates_processed_in_call ),
'processed_dates' => $dates_processed_in_call,
'remaining_dates' => $remaining_dates,
'estimated_calls_remaining' => $estimated_calls_remaining,
'date' => $sync_date,
'conversion_window_days' => $conversion_window_days,
'dates_synced' => $sync_dates,
'processed_records' => $processed_records_total,
'ad_groups_synced' => $ad_groups_synced_total,
'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
] );
exit;
}
static public function cron_campaigns_product_alerts_merchant() static public function cron_campaigns_product_alerts_merchant()
{ {
global $mdb, $settings; global $mdb, $settings;
self::$current_cron_action = __FUNCTION__;
self::touch_cron_invocation( __FUNCTION__ ); self::touch_cron_invocation( __FUNCTION__ );
$api = new \services\GoogleAdsApi(); $api = new \services\GoogleAdsApi();
@@ -2693,284 +2446,7 @@ class Cron
]; ];
} }
static private function count_pending_campaign_dates_for_client( $client_id )
{
global $mdb;
return (int) $mdb -> query(
"SELECT COUNT(*)
FROM cron_sync_status
WHERE pipeline = 'campaigns'
AND client_id = :client_id
AND phase = 'pending'
AND sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)",
[ ':client_id' => (int) $client_id ]
) -> fetchColumn();
}
static private function sync_campaigns_for_client( $client, $api, $as_of_date = null, $sync_details = true )
{
global $mdb;
$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, $as_of_date );
if ( $campaigns_30 === false )
{
$last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
$errors[] = 'Blad API dla klienta ' . $client['name'] . ' (ID: ' . $customer_id . '): ' . $last_err;
return [
'processed_records' => 0,
'ad_groups_synced' => 0,
'search_terms_synced' => 0,
'keywords_synced' => 0,
'negative_keywords_synced' => 0,
'alerts_synced' => 0,
'errors' => $errors
];
}
if ( !is_array( $campaigns_30 ) )
{
$campaigns_30 = [];
}
$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,
'keywords_synced' => 0,
'negative_keywords_synced' => 0,
'alerts_synced' => 0,
'errors' => $errors
];
}
$all_time_map = [];
$all_time_totals = [
'cost' => 0.0,
'conversion_value' => 0.0,
];
if ( is_array( $campaigns_all_time ) )
{
foreach ( $campaigns_all_time as $cat )
{
$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 );
}
}
$account_30_totals = [
'budget' => 0.0,
'money_spent' => 0.0,
'conversion_value' => 0.0,
];
foreach ( $campaigns_30 as $campaign )
{
$external_campaign_id = isset( $campaign['campaign_id'] ) ? (string) $campaign['campaign_id'] : '';
if ( $external_campaign_id === '' )
{
continue;
}
$advertising_channel_type = strtoupper( trim( (string) ( $campaign['advertising_channel_type'] ?? '' ) ) );
$account_30_totals['budget'] += (float) ( $campaign['budget'] ?? 0 );
$account_30_totals['money_spent'] += (float) ( $campaign['money_spent'] ?? 0 );
$account_30_totals['conversion_value'] += (float) ( $campaign['conversion_value'] ?? 0 );
if ( !$mdb -> count( 'campaigns', [ 'AND' => [
'client_id' => $client['id'],
'campaign_id' => $external_campaign_id
] ] ) )
{
$mdb -> insert( 'campaigns', [
'client_id' => $client['id'],
'campaign_id' => $external_campaign_id,
'campaign_name' => $campaign['campaign_name'],
'advertising_channel_type' => $advertising_channel_type !== '' ? $advertising_channel_type : null
] );
$db_campaign_id = $mdb -> id();
}
else
{
$db_campaign_id = $mdb -> get( 'campaigns', 'id', [ 'AND' => [
'client_id' => $client['id'],
'campaign_id' => $external_campaign_id
] ] );
$mdb -> update( 'campaigns', [
'campaign_name' => $campaign['campaign_name'],
'advertising_channel_type' => $advertising_channel_type !== '' ? $advertising_channel_type : null
], [ 'id' => $db_campaign_id ] );
}
$bidding_strategy = self::format_bidding_strategy(
$campaign['bidding_strategy'],
$campaign['target_roas'] ?? 0
);
$history_data = [
'roas_30_days' => $campaign['roas_30_days'],
'roas_all_time' => $all_time_map[ $external_campaign_id ] ?? 0,
'budget' => $campaign['budget'],
'money_spent' => $campaign['money_spent'],
'conversion_value' => $campaign['conversion_value'],
'bidding_strategy' => $bidding_strategy,
];
if ( $mdb -> count( 'campaigns_history', [ 'AND' => [
'campaign_id' => $db_campaign_id,
'date_add' => $as_of_date
] ] ) )
{
$mdb -> update( 'campaigns_history', $history_data, [ 'AND' => [
'campaign_id' => $db_campaign_id,
'date_add' => $as_of_date
] ] );
}
else
{
$history_data['campaign_id'] = $db_campaign_id;
$history_data['date_add'] = $as_of_date;
$mdb -> insert( 'campaigns_history', $history_data );
}
$campaigns_db_map[ $external_campaign_id ] = (int) $db_campaign_id;
$processed++;
}
$account_roas_30 = ( $account_30_totals['money_spent'] > 0 )
? round( ( $account_30_totals['conversion_value'] / $account_30_totals['money_spent'] ) * 100, 2 )
: 0;
$account_roas_all_time = ( $all_time_totals['cost'] > 0 )
? round( ( $all_time_totals['conversion_value'] / $all_time_totals['cost'] ) * 100, 2 )
: 0;
if ( !$mdb -> count( 'campaigns', [ 'AND' => [
'client_id' => $client['id'],
'campaign_id' => 0
] ] ) )
{
$mdb -> insert( 'campaigns', [
'client_id' => $client['id'],
'campaign_id' => 0,
'campaign_name' => '--- konto ---',
'advertising_channel_type' => null
] );
$db_account_campaign_id = $mdb -> id();
}
else
{
$db_account_campaign_id = $mdb -> get( 'campaigns', 'id', [ 'AND' => [
'client_id' => $client['id'],
'campaign_id' => 0
] ] );
$mdb -> update( 'campaigns', [
'campaign_name' => '--- konto ---',
'advertising_channel_type' => null
], [ 'id' => $db_account_campaign_id ] );
}
$account_history_data = [
'roas_30_days' => $account_roas_30,
'roas_all_time' => $account_roas_all_time,
'budget' => $account_30_totals['budget'],
'money_spent' => $account_30_totals['money_spent'],
'conversion_value' => $account_30_totals['conversion_value'],
'bidding_strategy' => 'Konto (agregacja wszystkich kampanii)',
];
if ( $mdb -> count( 'campaigns_history', [ 'AND' => [
'campaign_id' => $db_account_campaign_id,
'date_add' => $as_of_date
] ] ) )
{
$mdb -> update( 'campaigns_history', $account_history_data, [ 'AND' => [
'campaign_id' => $db_account_campaign_id,
'date_add' => $as_of_date
] ] );
}
else
{
$account_history_data['campaign_id'] = $db_account_campaign_id;
$account_history_data['date_add'] = $as_of_date;
$mdb -> insert( 'campaigns_history', $account_history_data );
}
$processed++;
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' => (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 );
$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,
[
'run_missing_mapping_alerts' => true,
'run_merchant_validation_alerts' => false
]
);
$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_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, $options = [] ) static private function sync_product_campaign_alerts_for_client( $client, $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $date_sync, $options = [] )
{ {
@@ -4868,6 +4344,7 @@ class Cron
static public function cron_facebook_ads() static public function cron_facebook_ads()
{ {
self::$current_cron_action = __FUNCTION__;
self::touch_cron_invocation( __FUNCTION__ ); self::touch_cron_invocation( __FUNCTION__ );
self::output_cron_response( self::run_facebook_ads_sync_payload( (int) \S::get( 'client_id' ) ) ); self::output_cron_response( self::run_facebook_ads_sync_payload( (int) \S::get( 'client_id' ) ) );
} }
@@ -5243,6 +4720,23 @@ class Cron
{ {
$payload = is_array( $payload ) ? $payload : [ 'result' => (string) $payload ]; $payload = is_array( $payload ) ? $payload : [ 'result' => (string) $payload ];
// --- Logowanie do tabeli logs ---
try
{
$has_errors = !empty( $payload['errors'] );
$log_level = $has_errors ? 'error' : 'info';
$log_source = self::$current_cron_action ?? 'cron';
$log_client_id = $payload['active_client_id'] ?? $payload['client_id'] ?? null;
$log_message = (string) ( $payload['result'] ?? 'Brak komunikatu' );
\factory\Logs::add( $log_level, $log_source, $log_message, $payload, $log_client_id );
\factory\Logs::cleanup_old( 30 );
}
catch ( \Throwable $e )
{
$payload['_log_error'] = $e -> getMessage();
}
if ( self::is_debug_requested() ) if ( self::is_debug_requested() )
{ {
header( 'Content-Type: text/html; charset=utf-8' ); header( 'Content-Type: text/html; charset=utf-8' );

View File

@@ -0,0 +1,151 @@
<?php
namespace controls;
class Logs
{
static public function main_view()
{
$sources = \factory\Logs::get_sources();
return \view\Logs::main_view( $sources );
}
static public function get_logs_data_table()
{
$start = (int) \S::get( 'start' );
$length = (int) \S::get( 'length' );
$draw = (int) \S::get( 'draw' );
$filters = [];
$level = trim( (string) \S::get( 'level' ) );
if ( $level !== '' && $level !== 'all' )
{
$filters['level'] = $level;
}
$source = trim( (string) \S::get( 'source' ) );
if ( $source !== '' && $source !== 'all' )
{
$filters['source'] = $source;
}
$date_from = trim( (string) \S::get( 'date_from' ) );
if ( $date_from !== '' )
{
$filters['date_from'] = $date_from;
}
$date_to = trim( (string) \S::get( 'date_to' ) );
if ( $date_to !== '' )
{
$filters['date_to'] = $date_to;
}
$rows = \factory\Logs::get_data( $start, $length, $filters );
$total = \factory\Logs::get_records_total( $filters );
$level_badges = [
'info' => '<span class="badge badge-success">info</span>',
'error' => '<span class="badge badge-danger">error</span>',
'warning' => '<span class="badge badge-warning">warning</span>'
];
$data = [];
foreach ( $rows as $row )
{
$message = (string) ( $row['message'] ?? '' );
if ( mb_strlen( $message ) > 120 )
{
$message = mb_substr( $message, 0, 120 ) . '...';
}
$client_label = '';
if ( !empty( $row['client_name'] ) )
{
$client_label = htmlspecialchars( (string) $row['client_name'], ENT_QUOTES, 'UTF-8' );
}
else if ( !empty( $row['client_id'] ) )
{
$client_label = 'ID: ' . (int) $row['client_id'];
}
else
{
$client_label = '-';
}
$data[] = [
(string) ( $row['date_add'] ?? '' ),
$level_badges[ $row['level'] ] ?? htmlspecialchars( (string) $row['level'], ENT_QUOTES, 'UTF-8' ),
htmlspecialchars( (string) ( $row['source'] ?? '' ), ENT_QUOTES, 'UTF-8' ),
$client_label,
htmlspecialchars( $message, ENT_QUOTES, 'UTF-8' ),
'<button type="button" class="btn btn-sm btn-outline-secondary log-detail-btn" data-id="' . (int) $row['id'] . '"><i class="fa-solid fa-eye"></i></button>'
];
}
echo json_encode( [
'draw' => $draw,
'recordsTotal' => $total,
'recordsFiltered' => $total,
'data' => $data
] );
exit;
}
static public function get_detail()
{
$id = (int) \S::get( 'id' );
if ( $id <= 0 )
{
echo json_encode( [ 'success' => false, 'message' => 'Nieprawidlowe ID.' ] );
exit;
}
$log = \factory\Logs::get_one( $id );
if ( !$log )
{
echo json_encode( [ 'success' => false, 'message' => 'Wpis nie znaleziony.' ] );
exit;
}
$context = null;
if ( !empty( $log['context_json'] ) )
{
$decoded = json_decode( $log['context_json'], true );
if ( $decoded !== null )
{
$context = json_encode( $decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE );
}
else
{
$context = $log['context_json'];
}
}
$response = json_encode( [
'success' => true,
'log' => [
'id' => (int) $log['id'],
'level' => $log['level'],
'source' => $log['source'],
'client_name' => $log['client_name'] ?? '',
'client_id' => $log['client_id'],
'message' => $log['message'],
'context' => $context,
'date_add' => $log['date_add']
]
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR );
if ( $response === false )
{
echo json_encode( [ 'success' => false, 'message' => 'Blad kodowania JSON: ' . json_last_error_msg() ] );
exit;
}
echo $response;
exit;
}
}

View File

@@ -330,7 +330,6 @@ class Users
[ 'name' => 'Cron uniwersalny (Google Ads)', 'path' => '/cron/cron_universal', 'action' => 'cron_universal', 'plan' => 'Co 1 min: kampanie (wczoraj) + frazy/produkty (7 dni wstecz) + Merchant URL' ], [ 'name' => 'Cron uniwersalny (Google Ads)', 'path' => '/cron/cron_universal', 'action' => 'cron_universal', 'plan' => 'Co 1 min: kampanie (wczoraj) + frazy/produkty (7 dni wstecz) + Merchant URL' ],
[ 'name' => 'Cron alertow kampanii (Merchant)', 'path' => '/cron/cron_campaigns_product_alerts_merchant', 'action' => 'cron_campaigns_product_alerts_merchant', 'plan' => 'Co 15 min: alerty produktowe z Google Merchant' ], [ 'name' => 'Cron alertow kampanii (Merchant)', 'path' => '/cron/cron_campaigns_product_alerts_merchant', 'action' => 'cron_campaigns_product_alerts_merchant', 'plan' => 'Co 15 min: alerty produktowe z Google Merchant' ],
[ 'name' => 'Cron URL produktów (Merchant)', 'path' => '/cron/cron_products_urls', 'action' => 'cron_products_urls', 'plan' => '' ], [ 'name' => 'Cron URL produktów (Merchant)', 'path' => '/cron/cron_products_urls', 'action' => 'cron_products_urls', 'plan' => '' ],
[ 'name' => 'Cron archiwum kampanii', 'path' => '/cron/cron_campaigns_archive', 'action' => 'cron_campaigns_archive', 'plan' => '' ],
[ 'name' => 'Cron Facebook Ads', 'path' => '/cron/cron_facebook_ads', 'action' => 'cron_facebook_ads', 'plan' => 'Co 5 min: 30 dni wstecz od wczoraj, blokada ponownego pobrania w tym samym dniu' ], [ 'name' => 'Cron Facebook Ads', 'path' => '/cron/cron_facebook_ads', 'action' => 'cron_facebook_ads', 'plan' => 'Co 5 min: 30 dni wstecz od wczoraj, blokada ponownego pobrania w tym samym dniu' ],
]; ];

View File

@@ -0,0 +1,156 @@
<?php
namespace factory;
class Logs
{
static public function add( $level, $source, $message, $context = null, $client_id = null )
{
global $mdb;
$level = trim( (string) $level );
if ( !in_array( $level, [ 'info', 'error', 'warning' ], true ) )
{
$level = 'info';
}
$context_json = null;
if ( $context !== null )
{
$context_json = json_encode( $context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR );
if ( $context_json === false )
{
$context_json = json_encode( [ '_encoding_error' => json_last_error_msg(), '_type' => gettype( $context ) ] );
}
}
$mdb -> insert( 'logs', [
'level' => $level,
'source' => trim( (string) $source ),
'client_id' => $client_id !== null ? (int) $client_id : null,
'message' => (string) $message,
'context_json' => $context_json
] );
return (int) $mdb -> id();
}
static public function get_data( $start, $length, $filters = [] )
{
global $mdb;
$start = max( 0, (int) $start );
$length = max( 1, min( 100, (int) $length ) );
$where_parts = [];
$params = [];
if ( !empty( $filters['level'] ) )
{
$where_parts[] = 'l.level = :level';
$params[':level'] = (string) $filters['level'];
}
if ( !empty( $filters['source'] ) )
{
$where_parts[] = 'l.source = :source';
$params[':source'] = (string) $filters['source'];
}
if ( !empty( $filters['date_from'] ) )
{
$where_parts[] = 'l.date_add >= :date_from';
$params[':date_from'] = (string) $filters['date_from'] . ' 00:00:00';
}
if ( !empty( $filters['date_to'] ) )
{
$where_parts[] = 'l.date_add <= :date_to';
$params[':date_to'] = (string) $filters['date_to'] . ' 23:59:59';
}
$where_sql = '';
if ( !empty( $where_parts ) )
{
$where_sql = 'WHERE ' . implode( ' AND ', $where_parts );
}
$sql = "SELECT l.*, c.name AS client_name
FROM logs AS l
LEFT JOIN clients AS c ON c.id = l.client_id
{$where_sql}
ORDER BY l.date_add DESC
LIMIT {$start}, {$length}";
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
}
static public function get_records_total( $filters = [] )
{
global $mdb;
$where_parts = [];
$params = [];
if ( !empty( $filters['level'] ) )
{
$where_parts[] = 'level = :level';
$params[':level'] = (string) $filters['level'];
}
if ( !empty( $filters['source'] ) )
{
$where_parts[] = 'source = :source';
$params[':source'] = (string) $filters['source'];
}
if ( !empty( $filters['date_from'] ) )
{
$where_parts[] = 'date_add >= :date_from';
$params[':date_from'] = (string) $filters['date_from'] . ' 00:00:00';
}
if ( !empty( $filters['date_to'] ) )
{
$where_parts[] = 'date_add <= :date_to';
$params[':date_to'] = (string) $filters['date_to'] . ' 23:59:59';
}
$where_sql = '';
if ( !empty( $where_parts ) )
{
$where_sql = 'WHERE ' . implode( ' AND ', $where_parts );
}
$row = $mdb -> query( "SELECT COUNT(*) AS cnt FROM logs {$where_sql}", $params ) -> fetch( \PDO::FETCH_ASSOC );
return (int) ( $row['cnt'] ?? 0 );
}
static public function get_one( $id )
{
global $mdb;
return $mdb -> query(
"SELECT l.*, c.name AS client_name
FROM logs AS l
LEFT JOIN clients AS c ON c.id = l.client_id
WHERE l.id = :id",
[ ':id' => (int) $id ]
) -> fetch( \PDO::FETCH_ASSOC );
}
static public function cleanup_old( $days = 30 )
{
global $mdb;
$days = max( 1, (int) $days );
$mdb -> query( "DELETE FROM logs WHERE date_add < DATE_SUB( NOW(), INTERVAL {$days} DAY )" );
}
static public function get_sources()
{
global $mdb;
$rows = $mdb -> query( "SELECT DISTINCT source FROM logs WHERE source != '' ORDER BY source ASC" ) -> fetchAll( \PDO::FETCH_COLUMN );
return is_array( $rows ) ? $rows : [];
}
}

View File

@@ -1952,6 +1952,7 @@ class GoogleAdsApi
. "segments.product_link, " . "segments.product_link, "
. "campaign.id, " . "campaign.id, "
. "campaign.name, " . "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, " . "campaign.advertising_channel_type, "
. "ad_group.id, " . "ad_group.id, "
. "ad_group.name, " . "ad_group.name, "
@@ -1962,6 +1963,7 @@ class GoogleAdsApi
. "metrics.conversions_value " . "metrics.conversions_value "
. "FROM shopping_performance_view " . "FROM shopping_performance_view "
. "WHERE segments.date = '" . $date . "' " . "WHERE segments.date = '" . $date . "' "
. "AND campaign.status = 'ENABLED' "
. "AND campaign.advertising_channel_type = 'SHOPPING'"; . "AND campaign.advertising_channel_type = 'SHOPPING'";
$gaql_with_ad_group = "SELECT " $gaql_with_ad_group = "SELECT "
@@ -1970,6 +1972,7 @@ class GoogleAdsApi
. "segments.product_title, " . "segments.product_title, "
. "campaign.id, " . "campaign.id, "
. "campaign.name, " . "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, " . "campaign.advertising_channel_type, "
. "ad_group.id, " . "ad_group.id, "
. "ad_group.name, " . "ad_group.name, "
@@ -1980,6 +1983,7 @@ class GoogleAdsApi
. "metrics.conversions_value " . "metrics.conversions_value "
. "FROM shopping_performance_view " . "FROM shopping_performance_view "
. "WHERE segments.date = '" . $date . "' " . "WHERE segments.date = '" . $date . "' "
. "AND campaign.status = 'ENABLED' "
. "AND campaign.advertising_channel_type = 'SHOPPING'"; . "AND campaign.advertising_channel_type = 'SHOPPING'";
$gaql_pmax_asset_group_with_url = "SELECT " $gaql_pmax_asset_group_with_url = "SELECT "
@@ -1989,6 +1993,7 @@ class GoogleAdsApi
. "segments.product_link, " . "segments.product_link, "
. "campaign.id, " . "campaign.id, "
. "campaign.name, " . "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, " . "campaign.advertising_channel_type, "
. "asset_group.id, " . "asset_group.id, "
. "asset_group.name, " . "asset_group.name, "
@@ -1999,6 +2004,7 @@ class GoogleAdsApi
. "metrics.conversions_value " . "metrics.conversions_value "
. "FROM asset_group_product_group_view " . "FROM asset_group_product_group_view "
. "WHERE segments.date = '" . $date . "' " . "WHERE segments.date = '" . $date . "' "
. "AND campaign.status = 'ENABLED' "
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'"; . "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'";
$gaql_pmax_asset_group = "SELECT " $gaql_pmax_asset_group = "SELECT "
@@ -2007,6 +2013,7 @@ class GoogleAdsApi
. "segments.product_title, " . "segments.product_title, "
. "campaign.id, " . "campaign.id, "
. "campaign.name, " . "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, " . "campaign.advertising_channel_type, "
. "asset_group.id, " . "asset_group.id, "
. "asset_group.name, " . "asset_group.name, "
@@ -2017,6 +2024,7 @@ class GoogleAdsApi
. "metrics.conversions_value " . "metrics.conversions_value "
. "FROM asset_group_product_group_view " . "FROM asset_group_product_group_view "
. "WHERE segments.date = '" . $date . "' " . "WHERE segments.date = '" . $date . "' "
. "AND campaign.status = 'ENABLED' "
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'"; . "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'";
$gaql_pmax_campaign_level_fallback_with_url = "SELECT " $gaql_pmax_campaign_level_fallback_with_url = "SELECT "
@@ -2026,6 +2034,7 @@ class GoogleAdsApi
. "segments.product_link, " . "segments.product_link, "
. "campaign.id, " . "campaign.id, "
. "campaign.name, " . "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, " . "campaign.advertising_channel_type, "
. "metrics.impressions, " . "metrics.impressions, "
. "metrics.clicks, " . "metrics.clicks, "
@@ -2034,6 +2043,7 @@ class GoogleAdsApi
. "metrics.conversions_value " . "metrics.conversions_value "
. "FROM shopping_performance_view " . "FROM shopping_performance_view "
. "WHERE segments.date = '" . $date . "' " . "WHERE segments.date = '" . $date . "' "
. "AND campaign.status = 'ENABLED' "
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'"; . "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'";
$gaql_pmax_campaign_level_fallback = "SELECT " $gaql_pmax_campaign_level_fallback = "SELECT "
@@ -2042,6 +2052,7 @@ class GoogleAdsApi
. "segments.product_title, " . "segments.product_title, "
. "campaign.id, " . "campaign.id, "
. "campaign.name, " . "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, " . "campaign.advertising_channel_type, "
. "metrics.impressions, " . "metrics.impressions, "
. "metrics.clicks, " . "metrics.clicks, "
@@ -2050,6 +2061,7 @@ class GoogleAdsApi
. "metrics.conversions_value " . "metrics.conversions_value "
. "FROM shopping_performance_view " . "FROM shopping_performance_view "
. "WHERE segments.date = '" . $date . "' " . "WHERE segments.date = '" . $date . "' "
. "AND campaign.status = 'ENABLED' "
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'"; . "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'";
$search_with_optional_url = function( $query_with_url, $query_without_url ) use ( $customer_id ) $search_with_optional_url = function( $query_with_url, $query_without_url ) use ( $customer_id )

View File

@@ -0,0 +1,12 @@
<?php
namespace view;
class Logs
{
static public function main_view( $sources = [] )
{
return \Tpl::view( 'logs/main_view', [
'sources' => $sources,
] );
}
}

View File

@@ -49,6 +49,9 @@ $route_aliases = [
'settings/save_claude' => ['users', 'settings_save_claude'], 'settings/save_claude' => ['users', 'settings_save_claude'],
'products/ai_suggest' => ['products', 'ai_suggest'], 'products/ai_suggest' => ['products', 'ai_suggest'],
'clients/save' => ['clients', 'save'], 'clients/save' => ['clients', 'save'],
'logs' => ['logs', 'main_view'],
'logs/get_data_table' => ['logs', 'get_logs_data_table'],
'logs/get_detail' => ['logs', 'get_detail'],
]; ];
$path = implode('/', $segments); $path = implode('/', $segments);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3532,3 +3532,213 @@ table#products {
color: #FFFFFF; color: #FFFFFF;
background: #2563EB; background: #2563EB;
} }
// ===========================
// LOGS PAGE
// ===========================
.logs-page {
.logs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: $cTextDark;
}
}
.logs-filters {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 14px;
margin-bottom: 16px;
.filter-group {
flex: 1 1 160px;
min-width: 0;
max-width: 220px;
label {
display: block;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #8899A6;
margin-bottom: 6px;
}
.form-control {
width: 100%;
padding: 8px 12px;
border: 1px solid $cBorder;
border-radius: 8px;
font-size: 14px;
color: $cTextDark;
background: $cWhite;
transition: border-color 0.2s;
&:focus {
outline: none;
border-color: $cPrimary;
box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);
}
}
select.form-control {
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238899A6' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
}
&.filter-group-buttons {
flex: 0 0 auto;
display: flex;
gap: 6px;
max-width: none;
}
}
}
.logs-table-wrap {
background: $cWhite;
border-radius: 10px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
overflow: hidden;
.table {
margin: 0;
thead th {
background: #F0F4FA;
border-bottom: 2px solid $cBorder;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #8899A6;
padding: 12px 14px;
white-space: nowrap;
}
tbody td {
padding: 10px 14px;
font-size: 13px;
color: $cTextDark;
vertical-align: middle;
border-bottom: 1px solid #EEF2F7;
}
tbody tr:hover td {
background: #F8FAFD;
}
}
.dt-layout-row {
padding: 14px 20px;
margin: 0 !important;
border-top: 1px solid #F1F5F9;
&:first-child {
display: none;
}
}
.dt-info {
font-size: 13px;
color: #8899A6;
}
.dt-paging {
.pagination {
margin: 0;
padding: 0;
list-style: none;
display: flex;
align-items: center;
gap: 6px;
.page-item {
.page-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 36px;
width: fit-content;
height: 36px;
padding: 0 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
border: 1px solid $cBorder;
background: $cWhite;
color: $cText;
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
line-height: 1;
white-space: nowrap;
&:hover {
background: #EEF2FF;
color: $cPrimary;
border-color: $cPrimary;
}
}
&.active .page-link {
background: $cPrimary;
color: $cWhite;
border-color: $cPrimary;
font-weight: 600;
}
&.disabled .page-link {
opacity: 0.35;
cursor: default;
pointer-events: none;
}
}
}
}
.dt-processing {
background: rgba($cWhite, 0.9);
color: $cText;
font-size: 14px;
}
}
.badge {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.badge-success {
background: #D1FAE5;
color: #065F46;
}
.badge-danger {
background: #FEE2E2;
color: #991B1B;
}
.badge-warning {
background: #FEF3C7;
color: #92400E;
}
}

View File

@@ -383,4 +383,15 @@
return new AdsProDialog( options ); return new AdsProDialog( options );
}; };
$.dialog = function( options )
{
options = options || {};
options.closeIcon = true;
if ( !options.buttons )
{
options.buttons = {};
}
return new AdsProDialog( options );
};
})( jQuery ); })( jQuery );

14
migrations/023_logs.sql Normal file
View File

@@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS `logs` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`level` varchar(20) NOT NULL DEFAULT 'info',
`source` varchar(120) NOT NULL DEFAULT '',
`client_id` int(11) DEFAULT NULL,
`message` text NOT NULL,
`context_json` longtext DEFAULT NULL,
`date_add` datetime NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `idx_logs_level` (`level`),
KEY `idx_logs_source` (`source`),
KEY `idx_logs_client` (`client_id`),
KEY `idx_logs_date` (`date_add`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

View File

@@ -44,7 +44,7 @@
<table class="table" id="products"> <table class="table" id="products">
<thead> <thead>
<tr> <tr>
<th style="width: 30px; text-align: center;"><input type="checkbox" id="select_all_history"></th> <th style="width: 30px; text-align: center;" data-orderable="false" data-dt-order="disable"><input type="checkbox" id="select_all_history"></th>
<th>Data</th> <th>Data</th>
<th>ROAS (30 dni)</th> <th>ROAS (30 dni)</th>
<th>ROAS (all time)</th> <th>ROAS (all time)</th>
@@ -61,11 +61,80 @@
</div> </div>
</div> </div>
<style type="text/css">
.campaigns-page input.ads-pretty-checkbox {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border: 1.5px solid #B8C5D6;
border-radius: 4px;
background: #FFFFFF;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
position: relative;
vertical-align: middle;
}
.campaigns-page input.ads-pretty-checkbox:hover {
border-color: #6690F4;
box-shadow: 0 0 0 3px rgba(102, 144, 244, 0.16);
}
.campaigns-page input.ads-pretty-checkbox:checked {
background: #6690F4;
border-color: #6690F4;
}
.campaigns-page input.ads-pretty-checkbox:checked::after {
content: '';
width: 8px;
height: 4px;
border: 2px solid #FFFFFF;
border-top: 0;
border-right: 0;
transform: rotate(-45deg) translate(1px, -1px);
}
.campaigns-page input.ads-pretty-checkbox:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(102, 144, 244, 0.26);
}
.campaigns-page #products thead th:first-child {
text-align: center;
}
</style>
<script type="text/javascript"> <script type="text/javascript">
var STORAGE_CLIENT_KEY = 'campaigns.last_client_id'; var STORAGE_CLIENT_KEY = 'campaigns.last_client_id';
var STORAGE_CAMPAIGN_KEY = 'campaigns.last_campaign_id'; var STORAGE_CAMPAIGN_KEY = 'campaigns.last_campaign_id';
var restore_campaign_after_client_load = ''; var restore_campaign_after_client_load = '';
if ( typeof $.fn.adsPrettyCheckbox === 'undefined' )
{
$.fn.adsPrettyCheckbox = function()
{
return this.each( function()
{
var input = $( this );
if ( !input.is( 'input[type="checkbox"]' ) )
return;
input.addClass( 'ads-pretty-checkbox' );
});
};
}
function initPrettyCheckboxes( context )
{
var root = context ? $( context ) : $( document );
root.find( 'input[type="checkbox"]' ).adsPrettyCheckbox();
}
function storage_set( key, value ) function storage_set( key, value )
{ {
try try
@@ -105,6 +174,7 @@ function rebuildCampaignDropdown()
menu.append( item ); menu.append( item );
}); });
initPrettyCheckboxes( menu );
updateCheckboxState(); updateCheckboxState();
updateDropdownDisplay(); updateDropdownDisplay();
} }
@@ -235,6 +305,8 @@ function reloadChart()
$( function() $( function()
{ {
initPrettyCheckboxes( '.campaigns-page' );
$( 'body' ).on( 'click', '#campaign_dropdown_trigger', function( e ) $( 'body' ).on( 'click', '#campaign_dropdown_trigger', function( e )
{ {
e.stopPropagation(); e.stopPropagation();
@@ -494,14 +566,14 @@ $( function()
pageLength: 15, pageLength: 15,
columns: [ columns: [
{ width: '30px', orderable: false, className: 'dt-center', searchable: false }, { width: '30px', orderable: false, className: 'dt-center', searchable: false },
{ width: '130px', name: 'date', orderable: false, className: "nowrap" }, { width: '130px', name: 'date', orderable: true, className: "nowrap" },
{ width: '120px', name: 'roas30', orderable: false, className: "dt-type-numeric" }, { width: '120px', name: 'roas30', orderable: true, className: "dt-type-numeric" },
{ width: '120px', name: 'roas_all_time', orderable: false, className: "dt-type-numeric" }, { width: '120px', name: 'roas_all_time', orderable: true, className: "dt-type-numeric" },
{ width: '180px', name: 'conversion_value', orderable: false, className: "dt-type-numeric" }, { width: '180px', name: 'conversion_value', orderable: true, className: "dt-type-numeric" },
{ width: '140px', name: 'spend30', orderable: false, className: "dt-type-numeric" }, { width: '140px', name: 'spend30', orderable: true, className: "dt-type-numeric" },
{ width: 'auto', name: 'comment', orderable: false }, { width: 'auto', name: 'comment', orderable: true },
{ width: 'auto', name: 'bidding_strategy', orderable: false }, { width: 'auto', name: 'bidding_strategy', orderable: true },
{ width: '100px', name: 'budget', orderable: false, className: "dt-type-numeric" }, { width: '100px', name: 'budget', orderable: true, className: "dt-type-numeric" },
{ width: '60px', name: 'actions', orderable: false, className: "dt-center" } { width: '60px', name: 'actions', orderable: false, className: "dt-center" }
], ],
language: { language: {
@@ -554,6 +626,7 @@ $( function()
$( 'body' ).on( 'draw.dt', '#products', function() $( 'body' ).on( 'draw.dt', '#products', function()
{ {
initPrettyCheckboxes( '#products' );
$( '#select_all_history' ).prop( 'checked', false ); $( '#select_all_history' ).prop( 'checked', false );
updateHistoryBulkActions(); updateHistoryBulkActions();
}); });

View File

@@ -0,0 +1,191 @@
<div class="logs-page">
<div class="logs-header">
<h2>Logi systemowe</h2>
</div>
<div class="logs-filters">
<div class="filter-group">
<label>Poziom</label>
<select id="filter_level" class="form-control">
<option value="all">Wszystkie</option>
<option value="error">Error</option>
<option value="warning">Warning</option>
<option value="info">Info</option>
</select>
</div>
<div class="filter-group">
<label>Zrodlo</label>
<select id="filter_source" class="form-control">
<option value="all">Wszystkie</option>
<?php foreach ( $this -> sources as $source ): ?>
<option value="<?= htmlspecialchars( $source, ENT_QUOTES, 'UTF-8' ); ?>"><?= htmlspecialchars( $source, ENT_QUOTES, 'UTF-8' ); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label>Data od</label>
<input type="date" id="filter_date_from" class="form-control">
</div>
<div class="filter-group">
<label>Data do</label>
<input type="date" id="filter_date_to" class="form-control">
</div>
<div class="filter-group filter-group-buttons">
<button type="button" id="filter_apply" class="btn btn-primary btn-sm">
<i class="fa-solid fa-filter"></i> Filtruj
</button>
<button type="button" id="filter_reset" class="btn btn-secondary btn-sm">
<i class="fa-solid fa-times"></i> Resetuj
</button>
</div>
</div>
<div class="logs-table-wrap">
<table class="table" id="logs-table">
<thead>
<tr>
<th>Data</th>
<th>Poziom</th>
<th>Zrodlo</th>
<th>Klient</th>
<th>Wiadomosc</th>
<th style="width: 50px; text-align: center;">Akcje</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<script type="text/javascript">
var logsTable = null;
function getFilterParams()
{
return {
level: $( '#filter_level' ).val(),
source: $( '#filter_source' ).val(),
date_from: $( '#filter_date_from' ).val(),
date_to: $( '#filter_date_to' ).val()
};
}
function initLogsTable()
{
if ( logsTable )
{
logsTable.destroy();
$( '#logs-table tbody' ).empty();
}
logsTable = new DataTable( '#logs-table', {
ajax: {
url: '/logs/get_data_table/',
data: function( d )
{
var f = getFilterParams();
d.level = f.level;
d.source = f.source;
d.date_from = f.date_from;
d.date_to = f.date_to;
}
},
processing: true,
serverSide: true,
searching: false,
lengthChange: false,
pageLength: 25,
order: [[ 0, 'desc' ]],
columns: [
{ width: '150px', orderable: false, className: 'nowrap' },
{ width: '70px', orderable: false, className: 'dt-center' },
{ width: '140px', orderable: false },
{ width: '140px', orderable: false },
{ orderable: false },
{ width: '50px', orderable: false, className: 'dt-center' }
],
language: {
processing: 'Ladowanie...',
emptyTable: 'Brak logow do wyswietlenia',
info: 'Wpisy _START_ - _END_ z _TOTAL_',
infoEmpty: '',
paginate: { previous: '&laquo;', next: '&raquo;' }
}
});
}
$( function()
{
initLogsTable();
$( '#filter_apply' ).on( 'click', function()
{
if ( logsTable )
{
logsTable.ajax.reload( null, true );
}
});
$( '#filter_reset' ).on( 'click', function()
{
$( '#filter_level' ).val( 'all' );
$( '#filter_source' ).val( 'all' );
$( '#filter_date_from' ).val( '' );
$( '#filter_date_to' ).val( '' );
if ( logsTable )
{
logsTable.ajax.reload( null, true );
}
});
$( 'body' ).on( 'click', '.log-detail-btn', function()
{
var id = $( this ).data( 'id' );
$.ajax({
url: '/logs/get_detail/',
type: 'GET',
dataType: 'json',
data: { id: id },
success: function( data )
{
if ( !data || !data.success )
{
$.alert({ title: 'Blad', content: ( data && data.message ) || 'Nie udalo sie pobrac szczegolow.', type: 'red' });
return;
}
var log = data.log;
var levelClass = log.level === 'error' ? 'danger' : ( log.level === 'warning' ? 'warning' : 'success' );
var html = '<div style="margin-bottom: 10px;">';
html += '<strong>Data:</strong> ' + $( '<span>' ).text( log.date_add ).html() + '<br>';
html += '<strong>Poziom:</strong> <span class="badge badge-' + levelClass + '">' + log.level + '</span><br>';
html += '<strong>Zrodlo:</strong> ' + $( '<span>' ).text( log.source || '-' ).html() + '<br>';
html += '<strong>Klient:</strong> ' + $( '<span>' ).text( log.client_name || ( log.client_id ? 'ID: ' + log.client_id : '-' ) ).html() + '<br>';
html += '<strong>Wiadomosc:</strong> ' + $( '<span>' ).text( log.message ).html();
html += '</div>';
if ( log.context )
{
html += '<div style="margin-top: 10px;">';
html += '<strong>Kontekst (JSON):</strong>';
html += '<pre style="background: #1e1e2e; color: #cdd6f4; padding: 12px; border-radius: 6px; max-height: 400px; overflow: auto; font-size: 12px; margin-top: 5px;">' + $( '<div>' ).text( log.context ).html() + '</pre>';
html += '</div>';
}
$.dialog({
title: 'Szczegoly logu #' + log.id,
content: html,
columnClass: 'xlarge'
});
},
error: function( xhr )
{
$.alert({ title: 'Blad', content: 'Blad pobierania szczegolow (HTTP ' + xhr.status + ')', type: 'red' });
}
});
});
});
</script>

View File

@@ -123,6 +123,12 @@
<span>Allegro import</span> <span>Allegro import</span>
</a> </a>
</li> </li>
<li class="<?= $module === 'logs' ? 'active' : '' ?>">
<a href="/logs">
<i class="fa-solid fa-file-lines"></i>
<span>Logi</span>
</a>
</li>
<li class="nav-divider"></li> <li class="nav-divider"></li>
<li class="<?= $module === 'users' ? 'active' : '' ?>"> <li class="<?= $module === 'users' ? 'active' : '' ?>">
<a href="/settings"> <a href="/settings">
@@ -165,6 +171,7 @@
'xml_files' => 'Pliki XML', 'xml_files' => 'Pliki XML',
'facebook_ads' => 'Facebook Ads', 'facebook_ads' => 'Facebook Ads',
'allegro' => 'Allegro import', 'allegro' => 'Allegro import',
'logs' => 'Logi',
'users' => 'Ustawienia', 'users' => 'Ustawienia',
]; ];
echo $breadcrumbs[$module] ?? 'adsPRO'; echo $breadcrumbs[$module] ?? 'adsPRO';