diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index 705b992..9e28af5 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -53,8 +53,8 @@ }, "class.Cron.php": { "type": "-", - "size": 114130, - "lmtime": 1771496221250, + "size": 116612, + "lmtime": 1771511676177, "modified": false }, "class.Products.php": { diff --git a/autoload/controls/class.Api.php b/autoload/controls/class.Api.php index d45112e..bf8a89f 100644 --- a/autoload/controls/class.Api.php +++ b/autoload/controls/class.Api.php @@ -448,7 +448,7 @@ class Api $history_30_rows++; } - $temp_rows = \controls\Cron::rebuild_products_temp_for_client( $client_id ); + $aggregate_rows = \controls\Cron::rebuild_products_temp_for_client( $client_id ); echo json_encode( [ 'status' => 'ok', @@ -457,7 +457,7 @@ class Api 'processed' => $processed, 'skipped' => $skipped, 'history_30_products' => $history_30_rows, - 'products_temp_rows' => $temp_rows + 'products_aggregate_rows' => $aggregate_rows ] ); exit; } diff --git a/autoload/controls/class.CampaignAlerts.php b/autoload/controls/class.CampaignAlerts.php index 7c759ee..8c5a9fe 100644 --- a/autoload/controls/class.CampaignAlerts.php +++ b/autoload/controls/class.CampaignAlerts.php @@ -3,6 +3,33 @@ namespace controls; class CampaignAlerts { + static private function redirect_to_main_view( $client_id = 0, $page = 1 ) + { + $client_id = (int) $client_id; + $page = max( 1, (int) $page ); + + $query = []; + + if ( $client_id > 0 ) + { + $query[] = 'client_id=' . $client_id; + } + + if ( $page > 1 ) + { + $query[] = 'page=' . $page; + } + + $url = '/campaign_alerts'; + if ( !empty( $query ) ) + { + $url .= '?' . implode( '&', $query ); + } + + header( 'Location: ' . $url ); + exit; + } + static public function main_view() { $client_id = (int) \S::get( 'client_id' ); @@ -31,4 +58,30 @@ class CampaignAlerts 'total' => $total ] ); } + + static public function delete_selected() + { + $client_id = (int) \S::get( 'client_id' ); + $page = max( 1, (int) \S::get( 'page' ) ); + $alert_ids = \S::get( 'alert_ids' ); + + if ( !is_array( $alert_ids ) || empty( $alert_ids ) ) + { + \S::alert( 'Nie zaznaczono alertow do usuniecia.' ); + self::redirect_to_main_view( $client_id, $page ); + } + + $deleted = \factory\CampaignAlerts::delete_alerts( $alert_ids ); + + if ( $deleted > 0 ) + { + \S::alert( $deleted === 1 ? 'Usunieto 1 alert.' : 'Usunieto ' . $deleted . ' alertow.' ); + } + else + { + \S::alert( 'Nie udalo sie usunac zaznaczonych alertow.' ); + } + + self::redirect_to_main_view( $client_id, $page ); + } } diff --git a/autoload/controls/class.Clients.php b/autoload/controls/class.Clients.php index 36dedd6..952f485 100644 --- a/autoload/controls/class.Clients.php +++ b/autoload/controls/class.Clients.php @@ -3,6 +3,51 @@ namespace controls; class Clients { + static private function clients_has_deleted_column() + { + global $mdb; + + static $has_deleted = null; + if ( $has_deleted !== null ) + { + return (bool) $has_deleted; + } + + try + { + $stmt = $mdb -> pdo -> prepare( 'SHOW COLUMNS FROM clients LIKE :column_name' ); + if ( $stmt ) + { + $stmt -> bindValue( ':column_name', 'deleted', \PDO::PARAM_STR ); + $stmt -> execute(); + $has_deleted = $stmt -> fetch( \PDO::FETCH_ASSOC ) ? 1 : 0; + } + else + { + $has_deleted = 0; + } + } + catch ( \Throwable $e ) + { + $has_deleted = 0; + } + + return (bool) $has_deleted; + } + + static private function sql_clients_not_deleted( $alias = '' ) + { + $alias = trim( (string) $alias ); + $prefix = $alias !== '' ? $alias . '.' : ''; + + if ( self::clients_has_deleted_column() ) + { + return 'COALESCE(' . $prefix . 'deleted, 0) = 0'; + } + + return '1=1'; + } + static public function main_view() { return \view\Clients::main_view( @@ -16,6 +61,8 @@ class Clients $name = trim( \S::get( 'name' ) ); $google_ads_customer_id = trim( \S::get( 'google_ads_customer_id' ) ); $google_merchant_account_id = trim( \S::get( 'google_merchant_account_id' ) ); + $active_raw = \S::get( 'active' ); + $active = (string) $active_raw === '0' ? 0 : 1; if ( !$name ) { @@ -31,6 +78,7 @@ class Clients 'google_ads_customer_id' => $google_ads_customer_id ?: null, 'google_merchant_account_id' => $google_merchant_account_id ?: null, 'google_ads_start_date' => $google_ads_start_date ?: null, + 'active' => $active, ]; if ( $id ) @@ -48,6 +96,36 @@ class Clients exit; } + static public function set_active() + { + $id = (int) \S::get( 'id' ); + $active = (int) \S::get( 'active' ) === 1 ? 1 : 0; + + if ( $id <= 0 ) + { + echo json_encode( [ 'success' => false, 'message' => 'Brak ID klienta.' ] ); + exit; + } + + $client = \factory\Clients::get( $id ); + if ( !$client ) + { + echo json_encode( [ 'success' => false, 'message' => 'Nie znaleziono klienta.' ] ); + exit; + } + + if ( (int) ( $client['deleted'] ?? 0 ) === 1 ) + { + echo json_encode( [ 'success' => false, 'message' => 'Nie mozna zmienic statusu usunietego klienta.' ] ); + exit; + } + + \factory\Clients::update( $id, [ 'active' => $active ] ); + + echo json_encode( [ 'success' => true, 'id' => $id, 'active' => $active ] ); + exit; + } + static public function delete() { $id = \S::get( 'id' ); @@ -73,6 +151,7 @@ class Clients static public function sync_status() { global $mdb; + $clients_not_deleted_sql = self::sql_clients_not_deleted(); // Kampanie: 1 work unit per row (pending=0, done=1) $campaigns_raw = $mdb->query( @@ -100,6 +179,32 @@ class Clients $data[ $row['client_id'] ]['products'] = [ (int) $row['done'], (int) $row['total'] ]; } + // Walidacja Merchant (cron_campaigns_product_alerts_merchant) dziala na kursorze klienta. + // Pokazujemy postep per klient jako 0/1 albo 1/1 w aktualnym cyklu. + $merchant_cursor_client_id = (int) \services\GoogleAdsApi::get_setting( 'cron_campaigns_product_alerts_last_client_id' ); + $merchant_clients_ids = $mdb -> query( + "SELECT id + FROM clients + WHERE " . $clients_not_deleted_sql . " + AND google_ads_customer_id IS NOT NULL + AND google_ads_customer_id <> '' + AND google_merchant_account_id IS NOT NULL + AND google_merchant_account_id <> '' + ORDER BY id ASC" + ) -> fetchAll( \PDO::FETCH_COLUMN ); + + foreach ( (array) $merchant_clients_ids as $merchant_client_id ) + { + $merchant_client_id = (int) $merchant_client_id; + if ( $merchant_client_id <= 0 ) + { + continue; + } + + $done = ( $merchant_cursor_client_id > 0 && $merchant_client_id <= $merchant_cursor_client_id ) ? 1 : 0; + $data[ $merchant_client_id ]['merchant'] = [ $done, 1 ]; + } + echo json_encode( [ 'status' => 'ok', 'data' => $data ] ); exit; } @@ -107,9 +212,10 @@ class Clients static public function force_sync() { global $mdb; + $clients_not_deleted_sql = self::sql_clients_not_deleted(); $id = (int) \S::get( 'id' ); - $pipeline = \S::get( 'pipeline' ); + $pipeline = trim( (string) \S::get( 'pipeline' ) ); if ( !$id ) { @@ -117,14 +223,62 @@ class Clients exit; } - $where = [ 'client_id' => $id ]; + $deleted_select = self::clients_has_deleted_column() ? 'COALESCE(deleted, 0) AS deleted' : '0 AS deleted'; + $client = $mdb -> query( + "SELECT id, COALESCE(active, 0) AS active, " . $deleted_select . ", google_ads_customer_id, google_merchant_account_id + FROM clients + WHERE id = :id + LIMIT 1", + [ ':id' => $id ] + ) -> fetch( \PDO::FETCH_ASSOC ); - if ( in_array( $pipeline, [ 'campaigns', 'products' ] ) ) + if ( !$client || (int) ( $client['deleted'] ?? 0 ) === 1 ) { - $where['pipeline'] = $pipeline; + echo json_encode( [ 'success' => false, 'message' => 'Nie znaleziono klienta.' ] ); + exit; } - $mdb -> delete( 'cron_sync_status', $where ); + if ( (int) ( $client['active'] ?? 0 ) !== 1 ) + { + echo json_encode( [ 'success' => false, 'message' => 'Klient jest nieaktywny. Aktywuj klienta przed synchronizacja.' ] ); + exit; + } + + if ( in_array( $pipeline, [ 'campaigns', 'products' ], true ) ) + { + $where = [ 'client_id' => $id ]; + $where['pipeline'] = $pipeline; + $mdb -> delete( 'cron_sync_status', $where ); + } + else if ( $pipeline === 'campaigns_product_alerts_merchant' ) + { + $has_ads_id = trim( (string) ( $client['google_ads_customer_id'] ?? '' ) ) !== ''; + $has_merchant_id = trim( (string) ( $client['google_merchant_account_id'] ?? '' ) ) !== ''; + if ( !$has_ads_id || !$has_merchant_id ) + { + echo json_encode( [ 'success' => false, 'message' => 'Klient nie ma kompletnego Google Ads Customer ID i Merchant Account ID.' ] ); + exit; + } + + $previous_eligible_id = (int) $mdb -> query( + "SELECT MAX(id) + FROM clients + WHERE " . $clients_not_deleted_sql . " + AND google_ads_customer_id IS NOT NULL + AND google_ads_customer_id <> '' + AND google_merchant_account_id IS NOT NULL + AND google_merchant_account_id <> '' + AND id < :client_id", + [ ':client_id' => $id ] + ) -> fetchColumn(); + + \services\GoogleAdsApi::set_setting( 'cron_campaigns_product_alerts_last_client_id', (string) max( 0, $previous_eligible_id ) ); + } + else + { + // Domyslny reset (wszystkie pipeline oparte o cron_sync_status). + $mdb -> delete( 'cron_sync_status', [ 'client_id' => $id ] ); + } echo json_encode( [ 'success' => true, 'pipeline' => $pipeline ?: 'all' ] ); exit; diff --git a/autoload/controls/class.Cron.php b/autoload/controls/class.Cron.php index af6ab16..9654f18 100644 --- a/autoload/controls/class.Cron.php +++ b/autoload/controls/class.Cron.php @@ -2,6 +2,486 @@ namespace controls; class Cron { + // Uniwersalny CRON pipeline. + // Jedno wywolanie = jeden klient + jeden dzien: kampanie -> produkty. + static public function cron_universal() + { + global $mdb; + + self::touch_cron_invocation( __FUNCTION__ ); + + $clients_not_deleted_sql = self::sql_clients_not_deleted(); + $clients_not_deleted_sql_c = self::sql_clients_not_deleted( 'c' ); + $clients_deleted_sql_c = self::sql_clients_deleted( 'c' ); + + $api = new \services\GoogleAdsApi(); + if ( !$api -> is_configured() ) + { + self::output_cron_response( [ 'result' => 'Google Ads API nie jest skonfigurowane. Uzupelnij dane w Ustawieniach.' ] ); + } + + $conversion_window_days = self::get_conversion_window_days( true ); + $default_end_date = date( 'Y-m-d', strtotime( '-2 days' ) ); + $requested_date_raw = trim( (string) \S::get( 'date' ) ); + $requested_ts = $requested_date_raw !== '' ? strtotime( $requested_date_raw ) : false; + $sync_date_raw = $requested_ts ? date( 'Y-m-d', $requested_ts ) : $default_end_date; + $sync_date = ( strtotime( $sync_date_raw ) > strtotime( $default_end_date ) ) ? $default_end_date : $sync_date_raw; + if ( !$sync_date || strtotime( $sync_date ) === false ) + { + $sync_date = $default_end_date; + } + + $sync_dates = self::build_backfill_dates( $sync_date, $conversion_window_days ); + $client_id = (int) \S::get( 'client_id' ); + + if ( $client_id > 0 ) + { + $client = $mdb -> query( + "SELECT * + FROM clients + WHERE id = :id + AND COALESCE(active, 0) = 1 + AND " . $clients_not_deleted_sql . " + LIMIT 1", + [ ':id' => $client_id ] + ) -> fetch( \PDO::FETCH_ASSOC ); + + if ( !$client || trim( (string) ( $client['google_ads_customer_id'] ?? '' ) ) === '' ) + { + self::output_cron_response( [ 'result' => 'Nie znaleziono aktywnego klienta z poprawnym Google Ads Customer ID.', 'client_id' => $client_id ] ); + } + + $campaign_errors = []; + $products_errors = []; + $processed_dates = [ $sync_date ]; + + $processed_records_total = 0; + $ad_groups_synced_total = 0; + $search_terms_history_synced_total = 0; + $search_terms_aggregated_total = 0; + $keywords_synced_total = 0; + $negative_keywords_synced_total = 0; + + $processed_products_total = 0; + $products_skipped_total = 0; + $history_30_products_total = 0; + $products_temp_rows_total = 0; + $products_fetch_skipped_reasons = []; + + $sync = self::sync_campaigns_snapshot_for_client( $client, $api, $sync_date ); + $processed_records_total += (int) ( $sync['processed_records'] ?? 0 ); + $ad_groups_synced_total += (int) ( $sync['ad_groups_synced'] ?? 0 ); + if ( !empty( $sync['errors'] ) ) + { + $campaign_errors = array_merge( $campaign_errors, (array) $sync['errors'] ); + } + + if ( empty( $campaign_errors ) ) + { + $terms_sync = self::sync_campaign_terms_backfill_for_client( + (int) $client['id'], + (string) ( $client['google_ads_customer_id'] ?? '' ), + $api, + [ $sync_date ] + ); + $search_terms_history_synced_total = (int) ( $terms_sync['history_synced'] ?? 0 ); + $search_terms_aggregated_total = (int) ( $terms_sync['aggregated'] ?? 0 ); + if ( !empty( $terms_sync['errors'] ) ) + { + $campaign_errors = array_merge( $campaign_errors, (array) $terms_sync['errors'] ); + } + } + + $last_day_in_window = end( $sync_dates ); + if ( empty( $campaign_errors ) && $last_day_in_window && $sync_date === $last_day_in_window ) + { + $kw_sync = self::sync_campaign_keywords_and_negatives_for_client( + (int) $client['id'], + (string) ( $client['google_ads_customer_id'] ?? '' ), + $api, + $sync_date + ); + $keywords_synced_total = (int) ( $kw_sync['keywords_synced'] ?? 0 ); + $negative_keywords_synced_total = (int) ( $kw_sync['negative_keywords_synced'] ?? 0 ); + if ( !empty( $kw_sync['errors'] ) ) + { + $campaign_errors = array_merge( $campaign_errors, (array) $kw_sync['errors'] ); + } + } + + $products_sync = self::sync_products_fetch_for_client( $client, $api, $sync_date ); + $processed_products_total += (int) ( $products_sync['processed_products'] ?? 0 ); + $products_skipped_total += (int) ( $products_sync['skipped'] ?? 0 ); + $products_fetch_skipped_reason = trim( (string) ( $products_sync['fetch_skipped_reason'] ?? '' ) ); + if ( $products_fetch_skipped_reason !== '' ) + { + $products_fetch_skipped_reasons[ $products_fetch_skipped_reason ] = true; + } + if ( !empty( $products_sync['errors'] ) ) + { + $products_errors = array_merge( $products_errors, (array) $products_sync['errors'] ); + } + + if ( empty( $products_errors ) ) + { + $history_30_products_total += (int) self::aggregate_products_history_30_for_client( (int) $client['id'], $sync_date ); + $products_temp_rows_total += (int) self::rebuild_products_temp_for_client( (int) $client['id'] ); + } + + $errors = array_merge( $campaign_errors, $products_errors ); + + self::output_cron_response( [ + 'result' => empty( $errors ) ? 'Synchronizacja uniwersalna zakonczona.' : 'Synchronizacja uniwersalna zakonczona z bledami.', + 'client_id' => (int) $client['id'], + 'active_date' => $sync_date, + 'conversion_window_days' => $conversion_window_days, + 'dates_synced' => $sync_dates, + 'processed_dates' => $processed_dates, + 'processed_records' => $processed_records_total, + 'ad_groups_synced' => $ad_groups_synced_total, + 'search_terms_history_synced' => $search_terms_history_synced_total, + 'search_terms_aggregated' => $search_terms_aggregated_total, + 'keywords_synced' => $keywords_synced_total, + 'negative_keywords_synced' => $negative_keywords_synced_total, + 'processed_products' => $processed_products_total, + 'products_skipped' => $products_skipped_total, + 'products_fetch_skipped_reasons' => array_keys( $products_fetch_skipped_reasons ), + 'history_30_products' => $history_30_products_total, + 'products_temp_rows' => $products_temp_rows_total, + 'errors' => $errors + ] ); + } + + self::cleanup_old_sync_rows( 30 ); + $mdb -> query( + "DELETE cs FROM cron_sync_status cs + LEFT JOIN clients c ON cs.client_id = c.id + WHERE cs.pipeline = 'campaigns' + AND ( c.id IS NULL OR " . $clients_deleted_sql_c . " OR COALESCE(c.active, 0) <> 1 )" + ); + $mdb -> query( + "DELETE cs FROM cron_sync_status cs + LEFT JOIN clients c ON cs.client_id = c.id + WHERE cs.pipeline = 'products' + AND ( c.id IS NULL OR " . $clients_deleted_sql_c . " OR COALESCE(c.active, 0) <> 1 )" + ); + + $client_ids = $mdb -> query( + "SELECT id + FROM clients + WHERE " . $clients_not_deleted_sql . " + AND COALESCE(active, 0) = 1 + AND TRIM(COALESCE(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 ) ) + { + self::output_cron_response( [ 'result' => 'Brak aktywnych klientow z ustawionym Google Ads Customer ID.' ] ); + } + + self::ensure_sync_rows( 'campaigns', $sync_dates, $client_ids ); + self::ensure_sync_rows( 'products', $sync_dates, $client_ids ); + self::cleanup_pipeline_rows_outside_window( 'campaigns', $sync_dates ); + self::cleanup_pipeline_rows_outside_window( 'products', $sync_dates ); + + $active_campaign_client_id = self::get_active_client( 'campaigns' ); + $active_products_client_id = self::get_active_client( 'products' ); + + if ( !$active_campaign_client_id && !$active_products_client_id ) + { + $merchant_client = $mdb -> query( + "SELECT c.* + FROM clients c + WHERE " . $clients_not_deleted_sql_c . " + AND COALESCE(c.active, 0) = 1 + AND TRIM(COALESCE(c.google_ads_customer_id, '')) <> '' + AND TRIM(COALESCE(c.google_merchant_account_id, '')) <> '' + AND EXISTS ( + SELECT 1 + FROM products p + WHERE p.client_id = c.id + AND TRIM(COALESCE(p.offer_id, '')) <> '' + AND ( + TRIM(COALESCE(p.product_url, '')) = '' + OR LOWER(TRIM(p.product_url)) IN ('0', '-', 'null') + ) + ) + ORDER BY c.id ASC + LIMIT 1" + ) -> fetch( \PDO::FETCH_ASSOC ); + + if ( is_array( $merchant_client ) && !empty( $merchant_client ) ) + { + $urls_sync = self::sync_products_urls_and_alerts_for_client( $merchant_client, $api, $sync_date ); + $merchant_errors = (array) ( $urls_sync['errors'] ?? [] ); + + self::output_cron_response( [ + 'result' => empty( $merchant_errors ) ? 'Synchronizacja URL produktow (Merchant) zakonczona.' : 'Synchronizacja URL produktow (Merchant) zakonczona z bledami.', + 'merchant_only' => 1, + 'active_client_id' => (int) ( $merchant_client['id'] ?? 0 ), + 'active_date' => $sync_date, + 'conversion_window_days' => $conversion_window_days, + 'dates_synced' => $sync_dates, + 'merchant_urls_checked' => (int) ( $urls_sync['checked_products'] ?? 0 ), + 'merchant_urls_updated' => (int) ( $urls_sync['updated_urls'] ?? 0 ), + 'merchant_missing_in_mc_count' => (int) ( $urls_sync['missing_in_merchant_count'] ?? 0 ), + 'merchant_missing_offer_ids' => array_values( array_unique( array_map( 'strval', (array) ( $urls_sync['missing_offer_ids'] ?? [] ) ) ) ), + 'errors' => $merchant_errors + ] ); + } + + self::output_cron_response( [ + 'result' => 'Wszyscy aktywni klienci zostali przetworzeni dla calego okna dat.', + 'active_date' => $sync_date, + 'conversion_window_days' => $conversion_window_days, + 'dates_synced' => $sync_dates, + 'processed_clients' => count( $client_ids ), + 'total_clients' => count( $client_ids ) + ] ); + } + + $active_client_id = $active_campaign_client_id ?: $active_products_client_id; + $dates_per_run = 1; + $campaign_dates_batch = []; + $products_dates_batch = []; + + if ( $active_campaign_client_id && $active_campaign_client_id === $active_client_id ) + { + $campaign_dates_batch = self::get_pending_dates_for_client( 'campaigns', $active_client_id, 'pending', 1 ); + } + if ( $active_products_client_id && $active_products_client_id === $active_client_id ) + { + $products_dates_batch = self::get_pending_dates_for_client( 'products', $active_client_id, 'pending', 1 ); + } + + if ( empty( $campaign_dates_batch ) && empty( $products_dates_batch ) ) + { + self::output_cron_response( [ + 'result' => 'Brak dat do przetworzenia dla aktywnego klienta. Kolejne wywolanie przejdzie dalej.', + 'active_client_id' => $active_client_id + ] ); + } + + $selected_client = $mdb -> query( + "SELECT * + FROM clients + WHERE id = :id + AND COALESCE(active, 0) = 1 + AND " . $clients_not_deleted_sql . " + LIMIT 1", + [ ':id' => $active_client_id ] + ) -> fetch( \PDO::FETCH_ASSOC ); + if ( !$selected_client || trim( (string) ( $selected_client['google_ads_customer_id'] ?? '' ) ) === '' ) + { + self::output_cron_response( [ + 'result' => 'Nie udalo sie znalezc aktywnego klienta do synchronizacji. ID: ' . $active_client_id, + 'active_client_id' => $active_client_id, + 'errors' => [ 'Klient ID ' . $active_client_id . ' nie znaleziony lub nieaktywny.' ] + ] ); + } + + $active_date = (string) ( $campaign_dates_batch[0] ?? ( $products_dates_batch[0] ?? '' ) ); + if ( $active_date === '' ) + { + self::output_cron_response( [ + 'result' => 'Brak daty do przetworzenia.', + 'active_client_id' => $active_client_id + ] ); + } + + $run_campaigns = !empty( $campaign_dates_batch ) && (string) $campaign_dates_batch[0] === $active_date; + $run_products = !empty( $products_dates_batch ) && (string) $products_dates_batch[0] === $active_date; + + $campaign_errors = []; + $products_errors = []; + $processed_records_total = 0; + $ad_groups_synced_total = 0; + $search_terms_history_synced_total = 0; + $search_terms_aggregated_total = 0; + $keywords_synced_total = 0; + $negative_keywords_synced_total = 0; + $processed_products_total = 0; + $products_skipped_total = 0; + $history_30_products_total = 0; + $products_temp_rows_total = 0; + $products_fetch_skipped_reasons = []; + + $products_sync_skipped_reason = ''; + + if ( $run_campaigns ) + { + $sync = self::sync_campaigns_snapshot_for_client( $selected_client, $api, $active_date ); + $processed_records_total += (int) ( $sync['processed_records'] ?? 0 ); + $ad_groups_synced_total += (int) ( $sync['ad_groups_synced'] ?? 0 ); + if ( !empty( $sync['errors'] ) ) + { + $campaign_errors = array_merge( $campaign_errors, (array) $sync['errors'] ); + } + + if ( empty( $campaign_errors ) ) + { + $terms_sync = self::sync_campaign_terms_backfill_for_client( + (int) $selected_client['id'], + (string) ( $selected_client['google_ads_customer_id'] ?? '' ), + $api, + [ $active_date ] + ); + $search_terms_history_synced_total = (int) ( $terms_sync['history_synced'] ?? 0 ); + $search_terms_aggregated_total = (int) ( $terms_sync['aggregated'] ?? 0 ); + if ( !empty( $terms_sync['errors'] ) ) + { + $campaign_errors = array_merge( $campaign_errors, (array) $terms_sync['errors'] ); + } + } + + $pending_dates_before_finalize = self::count_pending_campaign_dates_for_client( $active_client_id ); + $is_last_pending_chunk_for_client = $pending_dates_before_finalize <= 1; + if ( empty( $campaign_errors ) && $is_last_pending_chunk_for_client ) + { + $kw_sync = self::sync_campaign_keywords_and_negatives_for_client( + (int) $selected_client['id'], + (string) ( $selected_client['google_ads_customer_id'] ?? '' ), + $api, + $active_date + ); + $keywords_synced_total = (int) ( $kw_sync['keywords_synced'] ?? 0 ); + $negative_keywords_synced_total = (int) ( $kw_sync['negative_keywords_synced'] ?? 0 ); + if ( !empty( $kw_sync['errors'] ) ) + { + $campaign_errors = array_merge( $campaign_errors, (array) $kw_sync['errors'] ); + } + } + } + + if ( $run_products ) + { + $products_sync = self::sync_products_fetch_for_client( $selected_client, $api, $active_date ); + $processed_products_total += (int) ( $products_sync['processed_products'] ?? 0 ); + $products_skipped_total += (int) ( $products_sync['skipped'] ?? 0 ); + $products_fetch_skipped_reason = trim( (string) ( $products_sync['fetch_skipped_reason'] ?? '' ) ); + if ( $products_fetch_skipped_reason !== '' ) + { + $products_fetch_skipped_reasons[ $products_fetch_skipped_reason ] = true; + } + if ( !empty( $products_sync['errors'] ) ) + { + $products_errors = array_merge( $products_errors, (array) $products_sync['errors'] ); + } + + if ( empty( $products_errors ) ) + { + $history_30_products_total += (int) self::aggregate_products_history_30_for_client( $active_client_id, $active_date ); + $products_temp_rows_total += (int) self::rebuild_products_temp_for_client( $active_client_id ); + } + } + else + { + $products_sync_skipped_reason = 'already_done_for_day'; + } + + if ( $run_campaigns ) + { + $campaign_phase = empty( $campaign_errors ) ? 'done' : 'pending'; + $campaign_error_text = empty( $campaign_errors ) ? null : implode( '; ', array_values( array_unique( array_map( 'strval', $campaign_errors ) ) ) ); + self::mark_sync_phase( 'campaigns', $active_date, $active_client_id, $campaign_phase, $campaign_error_text ); + } + + if ( $run_products ) + { + $products_phase = empty( $products_errors ) ? 'done' : 'pending'; + $products_error_text = empty( $products_errors ) ? null : implode( '; ', array_values( array_unique( array_map( 'strval', $products_errors ) ) ) ); + self::mark_sync_phase( 'products', $active_date, $active_client_id, $products_phase, $products_error_text ); + } + + $errors = array_merge( $campaign_errors, $products_errors ); + + $campaign_done_count = (int) $mdb -> query( + "SELECT COUNT(*) + FROM cron_sync_status cs + INNER JOIN clients c ON cs.client_id = c.id + WHERE cs.pipeline = 'campaigns' + AND cs.client_id = :client_id + AND cs.phase = 'done' + AND " . $clients_not_deleted_sql_c . " + AND COALESCE(c.active, 0) = 1 + AND cs.sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)", + [ ':client_id' => $active_client_id ] + ) -> fetchColumn(); + $campaign_total_count = (int) $mdb -> query( + "SELECT COUNT(*) + FROM cron_sync_status cs + INNER JOIN clients c ON cs.client_id = c.id + WHERE cs.pipeline = 'campaigns' + AND cs.client_id = :client_id + AND " . $clients_not_deleted_sql_c . " + AND COALESCE(c.active, 0) = 1 + AND cs.sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)", + [ ':client_id' => $active_client_id ] + ) -> fetchColumn(); + + $products_done_count = (int) $mdb -> query( + "SELECT COUNT(*) + FROM cron_sync_status cs + INNER JOIN clients c ON cs.client_id = c.id + WHERE cs.pipeline = 'products' + AND cs.client_id = :client_id + AND cs.phase = 'done' + AND " . $clients_not_deleted_sql_c . " + AND COALESCE(c.active, 0) = 1 + AND cs.sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)", + [ ':client_id' => $active_client_id ] + ) -> fetchColumn(); + $products_total_count = (int) $mdb -> query( + "SELECT COUNT(*) + FROM cron_sync_status cs + INNER JOIN clients c ON cs.client_id = c.id + WHERE cs.pipeline = 'products' + AND cs.client_id = :client_id + AND " . $clients_not_deleted_sql_c . " + AND COALESCE(c.active, 0) = 1 + AND cs.sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)", + [ ':client_id' => $active_client_id ] + ) -> fetchColumn(); + + $campaign_remaining_dates = max( 0, $campaign_total_count - $campaign_done_count ); + $products_remaining_dates = max( 0, $products_total_count - $products_done_count ); + $remaining_dates = max( $campaign_remaining_dates, $products_remaining_dates ); + $estimated_calls_remaining = (int) ceil( $remaining_dates / max( 1, $dates_per_run ) ); + + self::output_cron_response( [ + 'result' => empty( $errors ) ? 'Synchronizacja uniwersalna zakonczona.' : 'Synchronizacja uniwersalna zakonczona z bledami.', + 'active_client_id' => $active_client_id, + 'dates_per_run' => $dates_per_run, + 'dates_processed_in_call' => 1, + 'processed_dates' => [ $active_date ], + 'campaigns_processed_in_call' => $run_campaigns ? 1 : 0, + 'products_processed_in_call' => $run_products ? 1 : 0, + 'remaining_dates' => $remaining_dates, + 'campaigns_remaining_dates' => $campaign_remaining_dates, + 'products_remaining_dates' => $products_remaining_dates, + 'estimated_calls_remaining' => $estimated_calls_remaining, + 'active_date' => $active_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_history_synced' => $search_terms_history_synced_total, + 'search_terms_aggregated' => $search_terms_aggregated_total, + 'keywords_synced' => $keywords_synced_total, + 'negative_keywords_synced' => $negative_keywords_synced_total, + 'processed_products' => $processed_products_total, + 'products_skipped' => $products_skipped_total, + 'products_fetch_skipped_reasons' => array_keys( $products_fetch_skipped_reasons ), + 'products_sync_skipped_reason' => $products_sync_skipped_reason, + 'history_30_products' => $history_30_products_total, + 'products_temp_rows' => $products_temp_rows_total, + 'total_clients' => count( $client_ids ), + 'errors' => $errors + ] ); + } + static public function cron_products() { global $mdb, $settings; @@ -482,9 +962,11 @@ class Cron $diag['products_total'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products WHERE client_id = ' . $client_id ) -> fetchColumn(); $diag['products_not_deleted'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products WHERE client_id = ' . $client_id ) -> fetchColumn(); $diag['products_with_offer_id'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products WHERE client_id = ' . $client_id . ' AND TRIM( COALESCE( offer_id, "" ) ) <> ""' ) -> fetchColumn(); - $diag['products_with_pd_rows'] = (int) $mdb -> query( 'SELECT COUNT( DISTINCT pd.product_id ) FROM products_data pd INNER JOIN products p ON p.id = pd.product_id WHERE p.client_id = ' . $client_id ) -> fetchColumn(); - $diag['products_with_real_url'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products p LEFT JOIN ( SELECT product_id, MAX( CASE WHEN TRIM( COALESCE( product_url, \"\" ) ) = \"\" THEN 0 WHEN LOWER( TRIM( product_url ) ) IN ( \"0\", \"-\", \"null\" ) THEN 0 ELSE 1 END ) AS has_real_url FROM products_data GROUP BY product_id ) pd ON pd.product_id = p.id WHERE p.client_id = ' . $client_id . ' AND TRIM( COALESCE( p.offer_id, \"\" ) ) <> \"\" AND COALESCE( pd.has_real_url, 0 ) = 1' ) -> fetchColumn(); - $diag['products_missing_url'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products p LEFT JOIN ( SELECT product_id, MAX( CASE WHEN TRIM( COALESCE( product_url, \"\" ) ) = \"\" THEN 0 WHEN LOWER( TRIM( product_url ) ) IN ( \"0\", \"-\", \"null\" ) THEN 0 ELSE 1 END ) AS has_real_url FROM products_data GROUP BY product_id ) pd ON pd.product_id = p.id WHERE p.client_id = ' . $client_id . ' AND TRIM( COALESCE( p.offer_id, \"\" ) ) <> \"\" AND COALESCE( pd.has_real_url, 0 ) = 0' ) -> fetchColumn(); + $diag['products_with_pd_rows'] = 0; + $diag['products_with_real_url'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products p WHERE p.client_id = ' . $client_id . ' AND TRIM( COALESCE( p.offer_id, \"\" ) ) <> \"\" AND TRIM( COALESCE( p.product_url, \"\" ) ) <> \"\" AND LOWER( TRIM( p.product_url ) ) NOT IN ( \"0\", \"-\", \"null\" )' ) -> fetchColumn(); + $diag['products_missing_url'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products p WHERE p.client_id = ' . $client_id . ' AND TRIM( COALESCE( p.offer_id, \"\" ) ) <> \"\" AND p.product_url IS NULL' ) -> fetchColumn(); + $diag['products_missing_url_pending_check'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products p WHERE p.client_id = ' . $client_id . ' AND TRIM( COALESCE( p.offer_id, \"\" ) ) <> \"\" AND p.product_url IS NULL AND COALESCE( p.merchant_url_not_found, 0 ) = 0' ) -> fetchColumn(); + $diag['products_missing_url_not_found'] = (int) $mdb -> query( 'SELECT COUNT(*) FROM products p WHERE p.client_id = ' . $client_id . ' AND TRIM( COALESCE( p.offer_id, \"\" ) ) <> \"\" AND p.product_url IS NULL AND COALESCE( p.merchant_url_not_found, 0 ) = 1' ) -> fetchColumn(); return $diag; } @@ -503,19 +985,10 @@ class Cron $sql = 'SELECT p.id AS product_id, p.offer_id ' . 'FROM products p ' - . 'LEFT JOIN ( ' - . ' SELECT product_id, ' - . ' MAX( CASE ' - . ' WHEN TRIM( COALESCE( product_url, \'\' ) ) = \'\' THEN 0 ' - . ' WHEN LOWER( TRIM( product_url ) ) IN ( \'0\', \'-\', \'null\' ) THEN 0 ' - . ' ELSE 1 ' - . ' END ) AS has_real_url ' - . ' FROM products_data ' - . ' GROUP BY product_id ' - . ') pd ON pd.product_id = p.id ' . 'WHERE p.client_id = ' . $client_id . ' ' . 'AND TRIM( COALESCE( p.offer_id, \'\' ) ) <> \'\' ' - . 'AND COALESCE( pd.has_real_url, 0 ) = 0 ' + . 'AND p.product_url IS NULL ' + . 'AND COALESCE( p.merchant_url_not_found, 0 ) = 0 ' . 'ORDER BY p.id ASC ' . 'LIMIT ' . $limit; @@ -523,6 +996,251 @@ class Cron return is_array( $rows ) ? $rows : []; } + static private function sync_products_urls_and_alerts_for_client( $client, $api, $date_sync = null, $limit = null ) + { + global $mdb, $settings; + + $client_id = (int) ( $client['id'] ?? 0 ); + $client_name = trim( (string) ( $client['name'] ?? '' ) ); + $merchant_account_id = trim( (string) ( $client['google_merchant_account_id'] ?? '' ) ); + $date_sync = $date_sync ? date( 'Y-m-d', strtotime( $date_sync ) ) : date( 'Y-m-d' ); + $alert_type = 'products_missing_in_merchant_center'; + + if ( $client_id <= 0 ) + { + return [ + 'checked_products' => 0, + 'updated_urls' => 0, + 'missing_in_merchant_count' => 0, + 'missing_offer_ids' => [], + 'missing_product_ids' => [], + 'errors' => [ 'Brak poprawnego klienta do synchronizacji URL.' ] + ]; + } + + if ( $merchant_account_id === '' ) + { + return [ + 'checked_products' => 0, + 'updated_urls' => 0, + 'missing_in_merchant_count' => 0, + 'missing_offer_ids' => [], + 'missing_product_ids' => [], + 'skipped_reason' => 'missing_merchant_account_id', + 'errors' => [] + ]; + } + + if ( !$api -> is_merchant_configured() ) + { + return [ + 'checked_products' => 0, + 'updated_urls' => 0, + 'missing_in_merchant_count' => 0, + 'missing_offer_ids' => [], + 'missing_product_ids' => [], + 'errors' => [ 'Merchant API nie jest skonfigurowane.' ] + ]; + } + + $limit = (int) $limit; + if ( $limit <= 0 ) + { + $limit = (int) ( $settings['cron_products_urls_limit_per_client'] ?? 200 ); + } + if ( $limit <= 0 ) + { + $limit = 200; + } + $limit = min( 2000, $limit ); + + $checked_products = 0; + $updated_urls = 0; + $missing_offer_ids_from_run = []; + $missing_product_ids_from_run = []; + $sync_errors = []; + + $selected_products = self::get_products_missing_url_for_client( $client_id, $limit ); + $checked_products = count( (array) $selected_products ); + + if ( $checked_products > 0 ) + { + $offer_ids = []; + foreach ( $selected_products as $row ) + { + $offer_id = trim( (string) ( $row['offer_id'] ?? '' ) ); + if ( $offer_id !== '' ) + { + $offer_ids[] = $offer_id; + } + } + $offer_ids = array_values( array_unique( $offer_ids ) ); + + $links_map = $api -> get_merchant_product_links_for_offer_ids( $merchant_account_id, $offer_ids ); + if ( $links_map === false ) + { + $last_err = trim( (string) \services\GoogleAdsApi::get_setting( 'google_merchant_last_error' ) ); + if ( $last_err === '' ) + { + $last_err = 'Nie udalo sie pobrac danych z Merchant Center.'; + } + $sync_errors[] = $last_err; + } + else + { + if ( !is_array( $links_map ) ) + { + $links_map = []; + } + + foreach ( $selected_products as $row ) + { + $offer_id = trim( (string) ( $row['offer_id'] ?? '' ) ); + $product_id = (int) ( $row['product_id'] ?? 0 ); + if ( $offer_id === '' || $product_id <= 0 ) + { + continue; + } + + $product_url = trim( (string) ( $links_map[ $offer_id ] ?? '' ) ); + if ( $product_url !== '' ) + { + \factory\Products::set_product_data( $product_id, 'product_url', $product_url ); + $updated_urls++; + continue; + } + + $missing_offer_ids_from_run[] = $offer_id; + $missing_product_ids_from_run[] = $product_id; + } + + $missing_product_ids_from_run = array_values( array_unique( array_map( 'intval', $missing_product_ids_from_run ) ) ); + if ( !empty( $missing_product_ids_from_run ) ) + { + $mdb -> update( 'products', [ + 'merchant_url_not_found' => 1, + 'merchant_url_last_check' => date( 'Y-m-d H:i:s' ) + ], [ 'id' => $missing_product_ids_from_run ] ); + } + } + } + + $not_found_rows = $mdb -> query( + "SELECT id AS product_id, offer_id, name, title + FROM products + WHERE client_id = :client_id + AND TRIM( COALESCE( offer_id, '' ) ) <> '' + AND product_url IS NULL + AND COALESCE( merchant_url_not_found, 0 ) = 1", + [ ':client_id' => $client_id ] + ) -> fetchAll( \PDO::FETCH_ASSOC ); + + $missing_offer_ids = []; + $missing_product_ids = []; + $missing_products = []; + foreach ( (array) $not_found_rows as $row ) + { + $offer_id = trim( (string) ( $row['offer_id'] ?? '' ) ); + $product_id = (int) ( $row['product_id'] ?? 0 ); + $product_name = trim( (string) ( $row['title'] ?? '' ) ); + if ( $product_name === '' ) + { + $product_name = trim( (string) ( $row['name'] ?? '' ) ); + } + if ( $product_name === '' ) + { + $product_name = $offer_id !== '' ? $offer_id : ( 'Produkt #' . $product_id ); + } + + if ( $offer_id !== '' ) + { + $missing_offer_ids[] = $offer_id; + } + if ( $product_id > 0 ) + { + $missing_product_ids[] = $product_id; + $missing_products[] = [ + 'product_id' => $product_id, + 'offer_id' => $offer_id, + 'product_name' => $product_name + ]; + } + } + + $missing_offer_ids = array_values( array_unique( array_map( 'strval', $missing_offer_ids ) ) ); + $missing_product_ids = array_values( array_unique( array_map( 'intval', $missing_product_ids ) ) ); + $missing_count = count( $missing_offer_ids ); + + // Odświeżamy dzienne alerty per produkt (1 alert = 1 produkt). + $mdb -> delete( 'campaign_alerts', [ + 'AND' => [ + 'client_id' => $client_id, + 'alert_type' => $alert_type, + 'date_detected' => $date_sync + ] + ] ); + + if ( $missing_count > 0 ) + { + foreach ( $missing_products as $product_row ) + { + $product_id = (int) ( $product_row['product_id'] ?? 0 ); + if ( $product_id <= 0 ) + { + continue; + } + + $offer_id = trim( (string) ( $product_row['offer_id'] ?? '' ) ); + $product_name = trim( (string) ( $product_row['product_name'] ?? '' ) ); + if ( $product_name === '' ) + { + $product_name = $offer_id !== '' ? $offer_id : ( 'Produkt #' . $product_id ); + } + + $message = 'Brak produktu w Merchant Center: "' . $product_name . '"'; + if ( $offer_id !== '' ) + { + $message .= ' (offer_id: ' . $offer_id . ')'; + } + $message .= '. Klient: "' . ( $client_name !== '' ? $client_name : ( 'ID ' . $client_id ) ) . '".'; + + $meta = [ + 'merchant_account_id' => $merchant_account_id, + 'product_id' => $product_id, + 'product_name' => $product_name, + 'offer_id' => $offer_id, + 'checked_products' => $checked_products, + 'updated_urls' => $updated_urls, + 'source' => 'cron_universal_products_urls' + ]; + + $mdb -> insert( 'campaign_alerts', [ + 'client_id' => $client_id, + 'campaign_id' => null, + // Techniczny scope alertu: 1 alert = 1 produkt. + 'campaign_external_id' => $product_id, + 'ad_group_id' => null, + 'ad_group_external_id' => 0, + 'product_id' => $product_id, + 'alert_type' => $alert_type, + 'message' => $message, + 'meta_json' => json_encode( $meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ), + 'date_detected' => $date_sync, + 'date_add' => date( 'Y-m-d H:i:s' ) + ] ); + } + } + + return [ + 'checked_products' => $checked_products, + 'updated_urls' => $updated_urls, + 'missing_in_merchant_count' => $missing_count, + 'missing_offer_ids' => $missing_offer_ids, + 'missing_product_ids' => $missing_product_ids, + 'errors' => $sync_errors + ]; + } + static private function sync_products_fetch_for_client( $client, $api, $date ) { global $mdb; @@ -583,7 +1301,7 @@ class Cron } $existing_products_rows = $mdb -> query( - 'SELECT id, offer_id, name + 'SELECT id, offer_id, name, title, product_url FROM products WHERE client_id = :client_id ORDER BY id ASC', @@ -601,30 +1319,9 @@ class Cron $products_by_offer_id[ $offer_id ] = [ 'id' => (int) ( $row['id'] ?? 0 ), - 'name' => (string) ( $row['name'] ?? '' ) - ]; - } - - $products_data_rows = $mdb -> query( - 'SELECT pd.id, pd.product_id, pd.product_url - FROM products_data AS pd - INNER JOIN products AS p ON p.id = pd.product_id - WHERE p.client_id = :client_id', - [ ':client_id' => $client_id ] - ) -> fetchAll( \PDO::FETCH_ASSOC ); - - $products_data_map = []; - foreach ( $products_data_rows as $row ) - { - $product_id = (int) ( $row['product_id'] ?? 0 ); - if ( $product_id <= 0 || isset( $products_data_map[ $product_id ] ) ) - { - continue; - } - - $products_data_map[ $product_id ] = [ - 'exists' => true, - 'product_url' => trim( (string) ( $row['product_url'] ?? '' ) ) + 'name' => (string) ( $row['name'] ?? '' ), + 'title' => (string) ( $row['title'] ?? '' ), + 'product_url' => (string) ( $row['product_url'] ?? '' ) ]; } @@ -901,7 +1598,8 @@ class Cron $products_by_offer_id[ $offer_external_id ] = [ 'id' => (int) $product_id, - 'name' => $product_title + 'name' => $product_title, + 'product_url' => '' ]; } else @@ -928,26 +1626,15 @@ class Cron if ( $product_url !== '' && filter_var( $product_url, FILTER_VALIDATE_URL ) && !$is_image_url ) { - $product_data_row = $products_data_map[ $product_id ] ?? [ 'exists' => false, 'product_url' => '' ]; - $existing_product_url = trim( (string) ( $product_data_row['product_url'] ?? '' ) ); + $existing_product_url = trim( (string) ( $products_by_offer_id[ $offer_external_id ]['product_url'] ?? '' ) ); if ( $existing_product_url !== $product_url ) { - if ( !empty( $product_data_row['exists'] ) ) - { - $mdb -> update( 'products_data', [ 'product_url' => $product_url ], [ 'product_id' => $product_id ] ); - } - else - { - $mdb -> insert( 'products_data', [ - 'product_id' => $product_id, - 'product_url' => $product_url - ] ); - $product_data_row['exists'] = true; - } - - $product_data_row['product_url'] = $product_url; - $products_data_map[ $product_id ] = $product_data_row; + $mdb -> update( 'products', [ + 'product_url' => $product_url, + 'merchant_url_not_found' => 0 + ], [ 'id' => $product_id ] ); + $products_by_offer_id[ $offer_external_id ]['product_url'] = $product_url; } } @@ -1211,6 +1898,12 @@ class Cron global $mdb; $client_id = (int) $client_id; + if ( $client_id <= 0 ) + { + return 0; + } + + $target_date = $date ? date( 'Y-m-d', strtotime( $date ) ) : date( 'Y-m-d', strtotime( '-1 days' ) ); $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 @@ -1221,7 +1914,7 @@ class Cron if ( $date ) { - $params[':date_add'] = date( 'Y-m-d', strtotime( $date ) ); + $params[':date_add'] = $target_date; $sql .= ' AND ph.date_add = :date_add'; } @@ -1257,6 +1950,107 @@ class Cron $processed++; } + self::rebuild_products_aggregate_for_client( $client_id, $target_date ); + + return $processed; + } + + static private function rebuild_products_aggregate_for_client( $client_id, $date_sync = null ) + { + global $mdb; + + $client_id = (int) $client_id; + if ( $client_id <= 0 ) + { + return 0; + } + + $date_sync = $date_sync ? date( 'Y-m-d', strtotime( $date_sync ) ) : date( 'Y-m-d', strtotime( '-1 days' ) ); + $date_from_30 = date( 'Y-m-d', strtotime( '-29 days', strtotime( $date_sync ) ) ); + + $product_ids = $mdb -> select( 'products', 'id', [ 'client_id' => $client_id ] ); + $product_ids = array_values( array_unique( array_map( 'intval', (array) $product_ids ) ) ); + if ( empty( $product_ids ) ) + { + return 0; + } + + $mdb -> delete( 'products_aggregate', [ 'product_id' => $product_ids ] ); + + $rows = $mdb -> query( + 'SELECT + ph.product_id, + ph.campaign_id, + ph.ad_group_id, + SUM( CASE WHEN ph.date_add BETWEEN :date_from_30 AND :date_sync THEN ph.impressions ELSE 0 END ) AS impressions_30, + SUM( CASE WHEN ph.date_add BETWEEN :date_from_30 AND :date_sync THEN ph.clicks ELSE 0 END ) AS clicks_30, + SUM( CASE WHEN ph.date_add BETWEEN :date_from_30 AND :date_sync THEN ph.cost ELSE 0 END ) AS cost_30, + SUM( CASE WHEN ph.date_add BETWEEN :date_from_30 AND :date_sync THEN ph.conversions ELSE 0 END ) AS conversions_30, + SUM( CASE WHEN ph.date_add BETWEEN :date_from_30 AND :date_sync THEN ph.conversions_value ELSE 0 END ) AS conversion_value_30, + SUM( CASE WHEN ph.date_add <= :date_sync THEN ph.impressions ELSE 0 END ) AS impressions_all_time, + SUM( CASE WHEN ph.date_add <= :date_sync THEN ph.clicks ELSE 0 END ) AS clicks_all_time, + SUM( CASE WHEN ph.date_add <= :date_sync THEN ph.cost ELSE 0 END ) AS cost_all_time, + SUM( CASE WHEN ph.date_add <= :date_sync THEN ph.conversions ELSE 0 END ) AS conversions_all_time, + SUM( CASE WHEN ph.date_add <= :date_sync THEN ph.conversions_value ELSE 0 END ) AS conversion_value_all_time + FROM products_history AS ph + INNER JOIN products AS p ON p.id = ph.product_id + WHERE p.client_id = :client_id + AND ph.campaign_id > 0 + AND ph.date_add <= :date_sync + GROUP BY ph.product_id, ph.campaign_id, ph.ad_group_id + HAVING + impressions_30 > 0 OR clicks_30 > 0 OR cost_30 > 0 OR conversions_30 > 0 OR conversion_value_30 > 0 + OR impressions_all_time > 0 OR clicks_all_time > 0 OR cost_all_time > 0 OR conversions_all_time > 0 OR conversion_value_all_time > 0', + [ + ':client_id' => $client_id, + ':date_from_30' => $date_from_30, + ':date_sync' => $date_sync + ] + ) -> fetchAll( \PDO::FETCH_ASSOC ); + + $processed = 0; + foreach ( (array) $rows as $row ) + { + $impressions_30 = (int) ( $row['impressions_30'] ?? 0 ); + $clicks_30 = (int) ( $row['clicks_30'] ?? 0 ); + $cost_30 = (float) ( $row['cost_30'] ?? 0 ); + $conversions_30 = (float) ( $row['conversions_30'] ?? 0 ); + $conversion_value_30 = (float) ( $row['conversion_value_30'] ?? 0 ); + $ctr_30 = $impressions_30 > 0 ? round( $clicks_30 / $impressions_30 * 100, 6 ) : 0; + $roas_30 = $cost_30 > 0 ? round( $conversion_value_30 / $cost_30 * 100, 6 ) : 0; + + $impressions_all = (int) ( $row['impressions_all_time'] ?? 0 ); + $clicks_all = (int) ( $row['clicks_all_time'] ?? 0 ); + $cost_all = (float) ( $row['cost_all_time'] ?? 0 ); + $conversions_all = (float) ( $row['conversions_all_time'] ?? 0 ); + $conversion_value_all = (float) ( $row['conversion_value_all_time'] ?? 0 ); + $ctr_all = $impressions_all > 0 ? round( $clicks_all / $impressions_all * 100, 6 ) : 0; + $roas_all = $cost_all > 0 ? round( $conversion_value_all / $cost_all * 100, 6 ) : 0; + + $mdb -> insert( 'products_aggregate', [ + 'product_id' => (int) ( $row['product_id'] ?? 0 ), + 'campaign_id' => (int) ( $row['campaign_id'] ?? 0 ), + 'ad_group_id' => (int) ( $row['ad_group_id'] ?? 0 ), + 'impressions_30' => $impressions_30, + 'clicks_30' => $clicks_30, + 'ctr_30' => $ctr_30, + 'cost_30' => $cost_30, + 'conversions_30' => $conversions_30, + 'conversion_value_30' => $conversion_value_30, + 'roas_30' => $roas_30, + 'impressions_all_time' => $impressions_all, + 'clicks_all_time' => $clicks_all, + 'ctr_all_time' => $ctr_all, + 'cost_all_time' => $cost_all, + 'conversions_all_time' => $conversions_all, + 'conversion_value_all_time' => $conversion_value_all, + 'roas_all_time' => $roas_all, + 'date_sync' => $date_sync + ] ); + + $processed++; + } + return $processed; } @@ -1270,51 +2064,24 @@ class Cron return 0; } + // products_temp zostalo wycofane. + // Zachowujemy logike aktualizacji custom_label_4 oparta o products_aggregate. $client_bestseller_min_roas = (int) \factory\Products::get_client_bestseller_min_roas( $client_id ); - $rows = $mdb -> query( - 'SELECT - p.id AS product_id, - p.name, - ph.campaign_id, - ph.ad_group_id, - COALESCE( SUM( ph.impressions ), 0 ) AS impressions, - COALESCE( SUM( ph.clicks ), 0 ) AS clicks, - COALESCE( SUM( ph.cost ), 0 ) AS cost, - COALESCE( SUM( ph.conversions ), 0 ) AS conversions, - COALESCE( SUM( ph.conversions_value ), 0 ) AS conversions_value - FROM products AS p - LEFT JOIN products_history AS ph ON p.id = ph.product_id AND ph.campaign_id > 0 - WHERE p.client_id = :client_id - GROUP BY p.id, p.name, ph.campaign_id, ph.ad_group_id', - [ ':client_id' => $client_id ] - ) -> fetchAll( \PDO::FETCH_ASSOC ); - - $product_ids = $mdb -> select( 'products', 'id', [ 'client_id' => $client_id ] ); - $product_ids = array_values( array_unique( array_map( 'intval', (array) $product_ids ) ) ); - - if ( !empty( $product_ids ) ) - { - $mdb -> delete( 'products_temp', [ 'product_id' => $product_ids ] ); - } - - // products_data jest globalne per product_id, wiec klasyfikacje liczymy globalnie. $global_totals = $mdb -> query( 'SELECT - p.id AS product_id, - COALESCE( SUM( ph.impressions ), 0 ) AS impressions, - COALESCE( SUM( ph.clicks ), 0 ) AS clicks, - COALESCE( SUM( ph.cost ), 0 ) AS cost, - COALESCE( SUM( ph.conversions ), 0 ) AS conversions, - COALESCE( SUM( ph.conversions_value ), 0 ) AS conversions_value - FROM products AS p - LEFT JOIN products_history AS ph ON p.id = ph.product_id AND ph.campaign_id > 0 + pa.product_id, + COALESCE( SUM( pa.cost_all_time ), 0 ) AS cost, + COALESCE( SUM( pa.conversions_all_time ), 0 ) AS conversions, + COALESCE( SUM( pa.conversion_value_all_time ), 0 ) AS conversions_value + FROM products_aggregate AS pa + INNER JOIN products AS p ON p.id = pa.product_id WHERE p.client_id = :client_id - GROUP BY p.id', + GROUP BY pa.product_id', [ ':client_id' => $client_id ] ) -> fetchAll( \PDO::FETCH_ASSOC ); - foreach ( $global_totals as $total ) + foreach ( (array) $global_totals as $total ) { $product_id = (int) ( $total['product_id'] ?? 0 ); if ( $product_id <= 0 ) @@ -1327,58 +2094,27 @@ class Cron $total_conversion_value = (float) ( $total['conversions_value'] ?? 0 ); $total_roas = ( $total_conversions > 0 && $total_cost > 0 ) ? round( $total_conversion_value / $total_cost, 2 ) * 100 : 0; - $custom_label_4 = \factory\Products::get_product_data( $product_id, 'custom_label_4' ); + $custom_label_4 = (string) $mdb -> get( 'products', 'custom_label_4', [ 'id' => $product_id ] ); if ( $custom_label_4 == null || ( $custom_label_4 == 'bestseller' && $client_bestseller_min_roas > 0 ) ) { $new_custom_label_4 = ( $total_roas > $client_bestseller_min_roas && $total_conversions > 10 ) ? 'bestseller' : null; - $offers_data_tmp = $mdb -> get( 'products_data', '*', [ 'product_id' => $product_id ] ); - if ( isset( $offers_data_tmp['id'] ) ) + $old_custom_label_4 = (string) $custom_label_4; + if ( $old_custom_label_4 !== (string) $new_custom_label_4 ) { - $old_custom_label_4 = (string) ( $offers_data_tmp['custom_label_4'] ?? '' ); + $mdb -> update( 'products', [ 'custom_label_4' => $new_custom_label_4 ], [ 'id' => $product_id ] ); + \factory\Products::set_product_data( $product_id, 'custom_label_4', $new_custom_label_4 ); - if ( $new_custom_label_4 != $offers_data_tmp['custom_label_4'] ) - { - $mdb -> insert( 'products_comments', [ - 'product_id' => $product_id, - 'comment' => 'Zmiana pola "custom_label_4" na: ' . $new_custom_label_4, - 'type' => 1, - 'date_add' => date( 'Y-m-d' ) - ] ); - } - - $mdb -> update( 'products_data', [ 'custom_label_4' => $new_custom_label_4 ], [ 'id' => $offers_data_tmp['id'] ] ); - - if ( $old_custom_label_4 !== (string) $new_custom_label_4 ) - { - \controls\Products::sync_product_fields_to_merchant( $product_id, [ - 'custom_label_4' => [ - 'old' => $old_custom_label_4, - 'new' => (string) $new_custom_label_4 - ] - ], 'cron_products' ); - } - } - else - { - $mdb -> insert( 'products_data', [ + $mdb -> insert( 'products_comments', [ 'product_id' => $product_id, - 'custom_label_4' => $new_custom_label_4 + 'comment' => 'Zmiana pola "custom_label_4" na: ' . (string) $new_custom_label_4, + 'type' => 1, + 'date_add' => date( 'Y-m-d' ) ] ); - if ( $new_custom_label_4 == 'bestseller' ) - { - $mdb -> insert( 'products_comments', [ - 'product_id' => $product_id, - 'comment' => 'Zmiana pola "custom_label_4" na: bestseller', - 'type' => 1, - 'date_add' => date( 'Y-m-d' ) - ] ); - } - \controls\Products::sync_product_fields_to_merchant( $product_id, [ 'custom_label_4' => [ - 'old' => '', + 'old' => $old_custom_label_4, 'new' => (string) $new_custom_label_4 ] ], 'cron_products' ); @@ -1386,56 +2122,14 @@ class Cron } } - $processed_rows = 0; - - foreach ( $rows as $row ) - { - $product_id = (int) ( $row['product_id'] ?? 0 ); - if ( $product_id <= 0 ) - { - continue; - } - - $campaign_id = (int) ( $row['campaign_id'] ?? 0 ); - $ad_group_id = (int) ( $row['ad_group_id'] ?? 0 ); - $impressions = (int) ( $row['impressions'] ?? 0 ); - $clicks = (int) ( $row['clicks'] ?? 0 ); - $cost = (float) ( $row['cost'] ?? 0 ); - $conversions = (float) ( $row['conversions'] ?? 0 ); - $conversions_value = (float) ( $row['conversions_value'] ?? 0 ); - - // Pomijamy puste scope bez danych. - if ( $impressions <= 0 && $clicks <= 0 && $cost <= 0 && $conversions <= 0 && $conversions_value <= 0 ) - { - continue; - } - - $cpc = $clicks > 0 ? round( $cost / $clicks, 6 ) : 0; - $roas = ( $conversions > 0 && $cost > 0 ) ? round( $conversions_value / $cost, 2 ) * 100 : 0; - $impressions_30 = (int) \factory\Products::get_impressions_30( $product_id, $campaign_id, $ad_group_id ); - $clicks_30 = (int) \factory\Products::get_clicks_30( $product_id, $campaign_id, $ad_group_id ); - - $mdb -> insert( 'products_temp', [ - 'product_id' => $product_id, - 'campaign_id' => $campaign_id, - 'ad_group_id' => $ad_group_id, - 'name' => $row['name'], - 'impressions' => $impressions, - 'impressions_30' => $impressions_30, - 'clicks' => $clicks, - 'clicks_30' => $clicks_30, - 'ctr' => ( $impressions > 0 ) ? round( $clicks / $impressions, 4 ) * 100 : 0, - 'cost' => $cost, - 'conversions' => $conversions, - 'conversions_value' => $conversions_value, - 'cpc' => $cpc, - 'roas' => $roas, - ] ); - - $processed_rows++; - } - - return $processed_rows; + // Zwracamy liczbe scope z tabeli products_aggregate (dla diagnostyki odpowiedzi cron). + return (int) $mdb -> query( + 'SELECT COUNT(*) + FROM products_aggregate pa + INNER JOIN products p ON p.id = pa.product_id + WHERE p.client_id = :client_id', + [ ':client_id' => $client_id ] + ) -> fetchColumn(); } static public function cron_products_history_30() @@ -1546,104 +2240,95 @@ class Cron ] ) -> fetchAll( \PDO::FETCH_ASSOC ); - // Inicjalizacja tablic do przechowywania danych - $offers_data = []; - - // Grupowanie danych wedug produktow - foreach ( $data as $entry ) + $day_count = count( $data ); + if ( $day_count <= 0 ) { - if ( !isset( $offers_data[$product_id] ) ) - { - $offers_data[$product_id] = [ - 'impressions' => 0, - 'clicks' => 0, - 'cost' => 0.0, - 'conversions' => 0, - 'conversions_value' => 0.0, - 'roas' => 0, - 'days_counted' => [] - ]; - } - - // Sumowanie danych wedug produktu - $offers_data[$product_id]['impressions'] += $entry['impressions']; - $offers_data[$product_id]['clicks'] += $entry['clicks']; - $offers_data[$product_id]['cost'] += $entry['cost']; - $offers_data[$product_id]['conversions'] += $entry['conversions']; - $offers_data[$product_id]['conversions_value'] += $entry['conversions_value']; - $offers_data[$product_id]['days_counted'][] = $entry['date_add']; + return; } - foreach ( $offers_data as $offer ) + $days_count_for_product = (int) $mdb -> query( + 'SELECT COUNT( DISTINCT date_add ) + FROM products_history + WHERE product_id = :product_id + AND campaign_id = :campaign_id + AND ad_group_id = :ad_group_id + AND date_add <= :date_to', + [ + ':product_id' => $product_id, + ':campaign_id' => $campaign_id, + ':ad_group_id' => $ad_group_id, + ':date_to' => $date_to + ] + ) -> fetchColumn(); + + if ( $days_count_for_product < 14 ) { - $day_count = count( $offer['days_counted'] ); + return; + } - $impressions = $offer['impressions']; - $clicks = $offer['clicks']; - $ctr = ( $clicks > 0 and $impressions ) ? round( $clicks / $impressions, 4 ) * 100 : 0; - $cost = $offer['cost']; - $conversions = $offer['conversions']; - $conversions_value = $offer['conversions_value']; - $roas = ( $conversions_value > 0 and $cost ) ? round( $conversions_value / $cost, 2 ) * 100 : 0; + $impressions_sum = 0; + $clicks_sum = 0; + $cost_sum = 0.0; + $conversions_sum = 0.0; + $conversions_value_sum = 0.0; - $days_count_for_product = (int) $mdb -> query( - 'SELECT COUNT( DISTINCT date_add ) - FROM products_history - WHERE product_id = :product_id - AND campaign_id = :campaign_id - AND ad_group_id = :ad_group_id - AND date_add <= :date_to', - [ - ':product_id' => $product_id, - ':campaign_id' => $campaign_id, - ':ad_group_id' => $ad_group_id, - ':date_to' => $date_to - ] - ) -> fetchColumn(); + foreach ( $data as $entry ) + { + $impressions_sum += (int) ( $entry['impressions'] ?? 0 ); + $clicks_sum += (int) ( $entry['clicks'] ?? 0 ); + $cost_sum += (float) ( $entry['cost'] ?? 0 ); + $conversions_sum += (float) ( $entry['conversions'] ?? 0 ); + $conversions_value_sum += (float) ( $entry['conversions_value'] ?? 0 ); + } - if ( $days_count_for_product >= 14 ) - { - if ( $mdb -> count( 'products_history_30', [ 'AND' => [ - 'product_id' => $product_id, - 'campaign_id' => $campaign_id, - 'ad_group_id' => $ad_group_id, - 'date_add' => $date_to - ] ] ) > 0 ) - { - $mdb -> update( 'products_history_30', [ - 'impressions' => $impressions, - 'clicks' => $clicks, - 'ctr' => $ctr, - 'cost' => $cost, - 'conversions' => $conversions, - 'conversions_value' => $conversions_value, - 'roas' => $roas, - 'roas_all_time' => self::get_roas_all_time( $product_id, $date_to, $campaign_id, $ad_group_id ) - ], [ 'AND' => [ - 'product_id' => $product_id, - 'campaign_id' => $campaign_id, - 'ad_group_id' => $ad_group_id, - 'date_add' => $date_to - ] ] ); - } - else - { - $mdb -> insert( 'products_history_30', [ - 'product_id' => $product_id, - 'campaign_id' => $campaign_id, - 'ad_group_id' => $ad_group_id, - 'impressions' => $impressions, - 'clicks' => $clicks, - 'ctr' => $ctr, - 'cost' => $cost, - 'conversions' => $conversions, - 'conversions_value' => $conversions_value, - 'roas' => $roas, - 'roas_all_time' => self::get_roas_all_time( $product_id, $date_to, $campaign_id, $ad_group_id ), - 'date_add' => $date_to - ] ); - } - } + // products_history_30 przechowuje srednie dzienne dla okna do 30 dni. + $impressions = (int) round( $impressions_sum / $day_count ); + $clicks = (int) round( $clicks_sum / $day_count ); + $cost = round( $cost_sum / $day_count, 6 ); + $conversions = round( $conversions_sum / $day_count, 6 ); + $conversions_value = round( $conversions_value_sum / $day_count, 6 ); + $ctr = ( $impressions > 0 ) ? round( $clicks / $impressions, 6 ) * 100 : 0; + $roas = ( $cost > 0 ) ? round( $conversions_value / $cost, 6 ) * 100 : 0; + + if ( $mdb -> count( 'products_history_30', [ 'AND' => [ + 'product_id' => $product_id, + 'campaign_id' => $campaign_id, + 'ad_group_id' => $ad_group_id, + 'date_add' => $date_to + ] ] ) > 0 ) + { + $mdb -> update( 'products_history_30', [ + 'impressions' => $impressions, + 'clicks' => $clicks, + 'ctr' => $ctr, + 'cost' => $cost, + 'conversions' => $conversions, + 'conversions_value' => $conversions_value, + 'roas' => $roas, + 'roas_all_time' => self::get_roas_all_time( $product_id, $date_to, $campaign_id, $ad_group_id ) + ], [ 'AND' => [ + 'product_id' => $product_id, + 'campaign_id' => $campaign_id, + 'ad_group_id' => $ad_group_id, + 'date_add' => $date_to + ] ] ); + } + else + { + $mdb -> insert( 'products_history_30', [ + 'product_id' => $product_id, + 'campaign_id' => $campaign_id, + 'ad_group_id' => $ad_group_id, + 'impressions' => $impressions, + 'clicks' => $clicks, + 'ctr' => $ctr, + 'cost' => $cost, + 'conversions' => $conversions, + 'conversions_value' => $conversions_value, + 'roas' => $roas, + 'roas_all_time' => self::get_roas_all_time( $product_id, $date_to, $campaign_id, $ad_group_id ), + 'date_add' => $date_to + ] ); } } @@ -1688,7 +2373,20 @@ class Cron return [ 'status' => 'error', 'message' => 'Nie znaleziono klienta o podanym ID.', 'client' => 'Nie istnieje' ]; } - $results = $mdb -> query( 'SELECT * FROM products AS p INNER JOIN products_data AS pd ON p.id = pd.product_id WHERE p.client_id = ' . $client_id ) -> fetchAll( \PDO::FETCH_ASSOC ); + $results = $mdb -> query( + 'SELECT + p.id AS product_id, + p.offer_id, + p.title, + p.description, + p.custom_label_4, + p.custom_label_3, + p.google_product_category, + p.product_url + FROM products AS p + WHERE p.client_id = :client_id', + [ ':client_id' => $client_id ] + ) -> fetchAll( \PDO::FETCH_ASSOC ); if ( empty( $results ) ) { @@ -1912,6 +2610,304 @@ class Cron // =========================== static public function cron_campaigns() + { + global $mdb, $settings; + self::touch_cron_invocation( __FUNCTION__ ); + $clients_not_deleted_sql = self::sql_clients_not_deleted(); + $clients_not_deleted_sql_c = self::sql_clients_not_deleted( 'c' ); + $clients_deleted_sql_c = self::sql_clients_deleted( 'c' ); + + $api = new \services\GoogleAdsApi(); + if ( !$api -> is_configured() ) + { + self::output_cron_response( [ 'result' => 'Google Ads API nie jest skonfigurowane. Uzupelnij dane w Ustawieniach.' ] ); + } + + $conversion_window_days = self::get_conversion_window_days( true ); + $default_end_date = date( 'Y-m-d', strtotime( '-2 days' ) ); + $requested_date_raw = trim( (string) \S::get( 'date' ) ); + $requested_ts = $requested_date_raw !== '' ? strtotime( $requested_date_raw ) : false; + $sync_date_raw = $requested_ts ? date( 'Y-m-d', $requested_ts ) : $default_end_date; + $sync_date = ( strtotime( $sync_date_raw ) > strtotime( $default_end_date ) ) ? $default_end_date : $sync_date_raw; + if ( !$sync_date || strtotime( $sync_date ) === false ) + { + $sync_date = $default_end_date; + } + + $sync_dates = self::build_backfill_dates( $sync_date, $conversion_window_days ); + $client_id = (int) \S::get( 'client_id' ); + + if ( $client_id > 0 ) + { + $client = $mdb -> query( + "SELECT * + FROM clients + WHERE id = :id + AND COALESCE(active, 0) = 1 + AND " . $clients_not_deleted_sql . " + LIMIT 1", + [ ':id' => $client_id ] + ) -> fetch( \PDO::FETCH_ASSOC ); + + if ( !$client || trim( (string) ( $client['google_ads_customer_id'] ?? '' ) ) === '' ) + { + self::output_cron_response( [ 'result' => 'Nie znaleziono aktywnego klienta z poprawnym Google Ads Customer ID.', 'client_id' => $client_id ] ); + } + + $processed_records_total = 0; + $ad_groups_synced_total = 0; + $search_terms_history_synced_total = 0; + $search_terms_aggregated_total = 0; + $keywords_synced_total = 0; + $negative_keywords_synced_total = 0; + $errors = []; + $processed_dates = []; + + $dates_processed_in_call = [ $sync_date ]; + foreach ( $dates_processed_in_call as $active_date ) + { + $sync = self::sync_campaigns_snapshot_for_client( $client, $api, $active_date ); + $processed_records_total += (int) ( $sync['processed_records'] ?? 0 ); + $ad_groups_synced_total += (int) ( $sync['ad_groups_synced'] ?? 0 ); + if ( !empty( $sync['errors'] ) ) + { + $errors = array_merge( $errors, (array) $sync['errors'] ); + } + $processed_dates[] = $active_date; + } + + if ( empty( $errors ) ) + { + $terms_sync = self::sync_campaign_terms_backfill_for_client( + (int) $client['id'], + (string) ( $client['google_ads_customer_id'] ?? '' ), + $api, + $dates_processed_in_call + ); + $search_terms_history_synced_total = (int) ( $terms_sync['history_synced'] ?? 0 ); + $search_terms_aggregated_total = (int) ( $terms_sync['aggregated'] ?? 0 ); + if ( !empty( $terms_sync['errors'] ) ) + { + $errors = array_merge( $errors, (array) $terms_sync['errors'] ); + } + } + + $last_day_in_window = end( $sync_dates ); + if ( empty( $errors ) && $last_day_in_window && $sync_date === $last_day_in_window ) + { + $kw_sync = self::sync_campaign_keywords_and_negatives_for_client( + (int) $client['id'], + (string) ( $client['google_ads_customer_id'] ?? '' ), + $api, + $sync_date + ); + $keywords_synced_total = (int) ( $kw_sync['keywords_synced'] ?? 0 ); + $negative_keywords_synced_total = (int) ( $kw_sync['negative_keywords_synced'] ?? 0 ); + if ( !empty( $kw_sync['errors'] ) ) + { + $errors = array_merge( $errors, (array) $kw_sync['errors'] ); + } + } + + self::output_cron_response( [ + 'result' => empty( $errors ) ? 'Synchronizacja kampanii zakonczona.' : 'Synchronizacja kampanii zakonczona z bledami.', + 'client_id' => (int) $client['id'], + 'active_date' => $sync_date, + 'conversion_window_days' => $conversion_window_days, + 'dates_synced' => $sync_dates, + 'processed_dates' => $processed_dates, + 'processed_records' => $processed_records_total, + 'ad_groups_synced' => $ad_groups_synced_total, + 'search_terms_history_synced' => $search_terms_history_synced_total, + 'search_terms_aggregated' => $search_terms_aggregated_total, + 'keywords_synced' => $keywords_synced_total, + 'negative_keywords_synced' => $negative_keywords_synced_total, + 'errors' => $errors + ] ); + } + + self::cleanup_old_sync_rows( 30 ); + $mdb -> query( + "DELETE cs FROM cron_sync_status cs + LEFT JOIN clients c ON cs.client_id = c.id + WHERE cs.pipeline = 'campaigns' + AND ( c.id IS NULL OR " . $clients_deleted_sql_c . " OR COALESCE(c.active, 0) <> 1 )" + ); + + $client_ids = $mdb -> query( + "SELECT id + FROM clients + WHERE " . $clients_not_deleted_sql . " + AND COALESCE(active, 0) = 1 + AND TRIM(COALESCE(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 ) ) + { + self::output_cron_response( [ 'result' => 'Brak aktywnych klientow z ustawionym Google Ads Customer ID.' ] ); + } + + self::ensure_sync_rows( 'campaigns', $sync_dates, $client_ids ); + self::cleanup_pipeline_rows_outside_window( 'campaigns', $sync_dates ); + + $active_client_id = self::get_active_client( 'campaigns' ); + if ( !$active_client_id ) + { + self::output_cron_response( [ + 'result' => 'Wszyscy aktywni klienci kampanii zostali przetworzeni dla calego okna dat.', + 'active_date' => $sync_date, + 'conversion_window_days' => $conversion_window_days, + 'dates_synced' => $sync_dates, + 'processed_clients' => count( $client_ids ), + 'total_clients' => count( $client_ids ) + ] ); + } + + $dates_per_run = 1; + $dates_batch = self::get_pending_dates_for_client( 'campaigns', $active_client_id, 'pending', 1 ); + if ( empty( $dates_batch ) ) + { + self::output_cron_response( [ + 'result' => 'Wszystkie daty klienta przetworzone. Kolejne wywolanie przejdzie do nastepnego klienta.', + 'active_client_id' => $active_client_id + ] ); + } + + $selected_client = $mdb -> query( + "SELECT * + FROM clients + WHERE id = :id + AND COALESCE(active, 0) = 1 + AND " . $clients_not_deleted_sql . " + LIMIT 1", + [ ':id' => $active_client_id ] + ) -> fetch( \PDO::FETCH_ASSOC ); + if ( !$selected_client || trim( (string) ( $selected_client['google_ads_customer_id'] ?? '' ) ) === '' ) + { + self::output_cron_response( [ + 'result' => 'Nie udalo sie znalezc aktywnego klienta do synchronizacji kampanii. ID: ' . $active_client_id, + 'active_client_id' => $active_client_id, + 'errors' => [ 'Klient ID ' . $active_client_id . ' nie znaleziony lub nieaktywny.' ] + ] ); + } + + $dates_processed_in_call = []; + $errors = []; + $processed_records_total = 0; + $ad_groups_synced_total = 0; + $search_terms_history_synced_total = 0; + $search_terms_aggregated_total = 0; + $keywords_synced_total = 0; + $negative_keywords_synced_total = 0; + + foreach ( $dates_batch as $active_date ) + { + $sync = self::sync_campaigns_snapshot_for_client( $selected_client, $api, $active_date ); + $processed_records_total += (int) ( $sync['processed_records'] ?? 0 ); + $ad_groups_synced_total += (int) ( $sync['ad_groups_synced'] ?? 0 ); + + $error_msg = null; + if ( !empty( $sync['errors'] ) ) + { + $errors = array_merge( $errors, (array) $sync['errors'] ); + } + $dates_processed_in_call[] = $active_date; + } + + if ( empty( $errors ) ) + { + $terms_sync = self::sync_campaign_terms_backfill_for_client( + (int) $selected_client['id'], + (string) ( $selected_client['google_ads_customer_id'] ?? '' ), + $api, + $dates_batch + ); + $search_terms_history_synced_total = (int) ( $terms_sync['history_synced'] ?? 0 ); + $search_terms_aggregated_total = (int) ( $terms_sync['aggregated'] ?? 0 ); + if ( !empty( $terms_sync['errors'] ) ) + { + $errors = array_merge( $errors, (array) $terms_sync['errors'] ); + } + } + + $pending_dates_before_finalize = self::count_pending_campaign_dates_for_client( $active_client_id ); + $is_last_pending_chunk_for_client = $pending_dates_before_finalize <= count( $dates_batch ); + if ( empty( $errors ) && $is_last_pending_chunk_for_client ) + { + $kw_sync = self::sync_campaign_keywords_and_negatives_for_client( + (int) $selected_client['id'], + (string) ( $selected_client['google_ads_customer_id'] ?? '' ), + $api, + (string) ( $dates_batch[ count( $dates_batch ) - 1 ] ?? $sync_date ) + ); + $keywords_synced_total = (int) ( $kw_sync['keywords_synced'] ?? 0 ); + $negative_keywords_synced_total = (int) ( $kw_sync['negative_keywords_synced'] ?? 0 ); + if ( !empty( $kw_sync['errors'] ) ) + { + $errors = array_merge( $errors, (array) $kw_sync['errors'] ); + } + } + + $final_phase = empty( $errors ) ? 'done' : 'pending'; + $final_error = empty( $errors ) ? null : implode( '; ', array_values( array_unique( array_map( 'strval', $errors ) ) ) ); + foreach ( $dates_batch as $active_date ) + { + self::mark_sync_phase( 'campaigns', $active_date, $active_client_id, $final_phase, $final_error ); + } + + $done_count = (int) $mdb -> query( + "SELECT COUNT(*) + FROM cron_sync_status cs + INNER JOIN clients c ON cs.client_id = c.id + WHERE cs.pipeline = 'campaigns' + AND cs.client_id = :client_id + AND cs.phase = 'done' + AND " . $clients_not_deleted_sql_c . " + AND COALESCE(c.active, 0) = 1 + AND cs.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 cs + INNER JOIN clients c ON cs.client_id = c.id + WHERE cs.pipeline = 'campaigns' + AND cs.client_id = :client_id + AND " . $clients_not_deleted_sql_c . " + AND COALESCE(c.active, 0) = 1 + AND cs.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 ) ); + + self::output_cron_response( [ + '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, + 'active_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_history_synced' => $search_terms_history_synced_total, + 'search_terms_aggregated' => $search_terms_aggregated_total, + 'keywords_synced' => $keywords_synced_total, + 'negative_keywords_synced' => $negative_keywords_synced_total, + 'total_clients' => count( $client_ids ), + 'errors' => $errors + ] ); + } + + // 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__ ); @@ -2112,6 +3108,1001 @@ class Cron exit; } + static public function cron_campaigns_product_alerts_merchant() + { + 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; + } + + if ( !$api -> is_merchant_configured() ) + { + echo json_encode( [ 'result' => 'Merchant 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' ); + $client_id = (int) \S::get( 'client_id' ); + + $where = "deleted = 0 + AND google_ads_customer_id IS NOT NULL AND google_ads_customer_id <> '' + AND google_merchant_account_id IS NOT NULL AND google_merchant_account_id <> ''"; + + if ( $client_id > 0 ) + { + $where .= ' AND id = ' . $client_id; + } + + $clients = $mdb -> query( + "SELECT id, name, google_ads_customer_id, google_merchant_account_id + FROM clients + WHERE " . $where . " + ORDER BY id ASC" + ) -> fetchAll( \PDO::FETCH_ASSOC ); + + if ( !is_array( $clients ) || empty( $clients ) ) + { + echo json_encode( [ + 'result' => 'Brak klientow z Google Ads Customer ID i Merchant Account ID.', + 'processed_clients' => 0, + 'alerts_synced' => 0, + 'errors' => [] + ] ); + exit; + } + + $clients_per_run_default = (int) ( $settings['cron_campaigns_product_alerts_clients_per_run'] ?? ( $settings['cron_campaigns_clients_per_run'] ?? 2 ) ); + if ( $clients_per_run_default <= 0 ) + { + $clients_per_run_default = 2; + } + + $clients_per_run = (int) \S::get( 'clients_per_run' ); + if ( $clients_per_run <= 0 ) + { + $clients_per_run = (int) self::get_setting_value( 'cron_campaigns_product_alerts_clients_per_run', $clients_per_run_default ); + } + if ( $clients_per_run <= 0 ) + { + $clients_per_run = $clients_per_run_default; + } + $clients_per_run = min( 20, $clients_per_run ); + + $total_clients_available = count( $clients ); + if ( $client_id <= 0 ) + { + $last_client_cursor = (int) self::get_setting_value( 'cron_campaigns_product_alerts_last_client_id', 0 ); + $clients = self::pick_clients_batch_by_cursor( $clients, $clients_per_run, $last_client_cursor ); + } + else + { + $clients_per_run = 1; + } + + $processed_clients = 0; + $alerts_synced = 0; + $errors = []; + $details = []; + $last_client_id = 0; + + foreach ( (array) $clients as $client ) + { + $processed_clients++; + $last_client_id = (int) ( $client['id'] ?? 0 ); + + $campaigns_db_map = self::build_campaigns_db_map_for_client( (int) ( $client['id'] ?? 0 ) ); + if ( empty( $campaigns_db_map ) ) + { + $details[] = [ + 'client_id' => (int) ( $client['id'] ?? 0 ), + 'client_name' => (string) ( $client['name'] ?? '' ), + 'alerts_synced' => 0, + 'note' => 'Brak kampanii w bazie dla klienta.' + ]; + continue; + } + + $ad_group_db_map = self::build_ad_group_db_map_from_db( $campaigns_db_map ); + + $sync = self::sync_product_campaign_alerts_for_client( + $client, + $campaigns_db_map, + $ad_group_db_map, + (string) ( $client['google_ads_customer_id'] ?? '' ), + $api, + $sync_date, + [ + 'run_missing_mapping_alerts' => false, + 'run_merchant_validation_alerts' => true + ] + ); + + $alerts_synced += (int) ( $sync['count'] ?? 0 ); + if ( !empty( $sync['errors'] ) ) + { + $errors = array_merge( $errors, (array) $sync['errors'] ); + } + + $details[] = [ + 'client_id' => (int) ( $client['id'] ?? 0 ), + 'client_name' => (string) ( $client['name'] ?? '' ), + 'alerts_synced' => (int) ( $sync['count'] ?? 0 ), + 'errors_count' => count( (array) ( $sync['errors'] ?? [] ) ) + ]; + } + + if ( $client_id <= 0 && $last_client_id > 0 ) + { + self::set_setting_value( 'cron_campaigns_product_alerts_last_client_id', (string) $last_client_id ); + } + + echo json_encode( [ + 'result' => empty( $errors ) ? 'Walidacja Merchant dla alertow kampanii zakonczona.' : 'Walidacja Merchant zakonczona z bledami.', + 'date' => $sync_date, + 'processed_clients' => $processed_clients, + 'clients_per_run' => $clients_per_run, + 'total_clients_available' => $total_clients_available, + 'alerts_synced' => $alerts_synced, + 'errors' => $errors, + 'details' => $details + ] ); + exit; + } + + static private function build_campaigns_db_map_for_client( $client_id ) + { + global $mdb; + + $client_id = (int) $client_id; + if ( $client_id <= 0 ) + { + return []; + } + + $rows = $mdb -> query( + "SELECT id, campaign_id + FROM campaigns + WHERE client_id = :client_id + AND campaign_id > 0", + [ ':client_id' => $client_id ] + ) -> fetchAll( \PDO::FETCH_ASSOC ); + + $map = []; + foreach ( (array) $rows as $row ) + { + $campaign_external_id = (string) ( (int) ( $row['campaign_id'] ?? 0 ) ); + $db_campaign_id = (int) ( $row['id'] ?? 0 ); + if ( $campaign_external_id === '0' || $db_campaign_id <= 0 ) + { + continue; + } + $map[ $campaign_external_id ] = $db_campaign_id; + } + + return $map; + } + + static public function cron_clients_bundle() + { + global $mdb; + 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_raw = \S::get( 'date' ) ? date( 'Y-m-d', strtotime( \S::get( 'date' ) ) ) : date( 'Y-m-d' ); + $today_date = date( 'Y-m-d' ); + $yesterday_date = date( 'Y-m-d', strtotime( '-1 days', strtotime( $today_date ) ) ); + $sync_date = ( strtotime( $sync_date_raw ) >= strtotime( $today_date ) ) ? $yesterday_date : $sync_date_raw; + if ( strtotime( $sync_date ) === false ) + { + $sync_date = $yesterday_date; + } + + $conversion_window_days = (int) \S::get( 'window_days' ); + if ( $conversion_window_days <= 0 ) + { + $conversion_window_days = (int) self::get_setting_value( 'cron_clients_bundle_window_days', 7 ); + } + if ( $conversion_window_days <= 0 ) + { + $conversion_window_days = 7; + } + $conversion_window_days = min( 90, max( 1, $conversion_window_days ) ); + $sync_dates = self::build_backfill_dates( $sync_date, $conversion_window_days ); + if ( empty( $sync_dates ) ) + { + $sync_dates = [ $sync_date ]; + } + $dates_total = count( $sync_dates ); + + $forced_client_id = (int) \S::get( 'client_id' ); + $reset_state = (int) \S::get( 'reset' ) === 1; + $max_stage_retries = (int) \S::get( 'max_retries' ); + if ( $max_stage_retries <= 0 ) + { + $max_stage_retries = (int) self::get_setting_value( 'cron_clients_bundle_max_stage_retries', 3 ); + } + if ( $max_stage_retries <= 0 ) + { + $max_stage_retries = 3; + } + $max_stage_retries = min( 20, $max_stage_retries ); + + $where = "deleted = 0 + AND google_ads_customer_id IS NOT NULL + AND google_ads_customer_id <> ''"; + if ( $forced_client_id > 0 ) + { + $where .= ' AND id = ' . $forced_client_id; + } + + $clients = $mdb -> query( + "SELECT id, name, google_ads_customer_id, google_merchant_account_id + FROM clients + WHERE " . $where . " + ORDER BY id ASC" + ) -> fetchAll( \PDO::FETCH_ASSOC ); + + if ( !is_array( $clients ) || empty( $clients ) ) + { + self::set_setting_value( 'cron_clients_bundle_stage', 'campaigns' ); + self::set_setting_value( 'cron_clients_bundle_client_id', '0' ); + self::set_setting_value( 'cron_clients_bundle_date_index', '0' ); + + echo json_encode( [ + 'result' => 'Brak klientow z Google Ads Customer ID do przetworzenia.', + 'date' => $sync_date, + 'processed_stage' => 'none', + 'errors' => [] + ] ); + exit; + } + + $stage_order = [ 'campaigns', 'products', 'merchant' ]; + $saved_stage = trim( (string) self::get_setting_value( 'cron_clients_bundle_stage', 'campaigns' ) ); + if ( !in_array( $saved_stage, $stage_order, true ) ) + { + $saved_stage = 'campaigns'; + } + + $saved_client_id = (int) self::get_setting_value( 'cron_clients_bundle_client_id', 0 ); + $saved_date_index = (int) self::get_setting_value( 'cron_clients_bundle_date_index', 0 ); + $client_ids = array_values( array_map( function( $row ) + { + return (int) ( $row['id'] ?? 0 ); + }, (array) $clients ) ); + + self::cleanup_old_sync_rows( 30 ); + self::ensure_sync_rows( 'campaigns', $sync_dates, $client_ids ); + self::ensure_sync_rows( 'products', $sync_dates, $client_ids ); + + if ( !in_array( $saved_client_id, $client_ids, true ) ) + { + $saved_client_id = (int) ( $client_ids[0] ?? 0 ); + $saved_stage = 'campaigns'; + $saved_date_index = 0; + } + + if ( $reset_state ) + { + $saved_stage = 'campaigns'; + $saved_client_id = $forced_client_id > 0 ? $forced_client_id : (int) ( $client_ids[0] ?? 0 ); + $saved_date_index = 0; + if ( !in_array( $saved_client_id, $client_ids, true ) ) + { + $saved_client_id = (int) ( $client_ids[0] ?? 0 ); + } + + self::set_setting_value( 'cron_clients_bundle_stage', $saved_stage ); + self::set_setting_value( 'cron_clients_bundle_client_id', (string) $saved_client_id ); + self::set_setting_value( 'cron_clients_bundle_date_index', '0' ); + self::set_setting_value( 'cron_clients_bundle_retry_signature', '' ); + self::set_setting_value( 'cron_clients_bundle_retry_count', '0' ); + self::set_setting_value( 'cron_clients_bundle_last_error', '' ); + } + + $current_client = null; + foreach ( (array) $clients as $client_row ) + { + if ( (int) ( $client_row['id'] ?? 0 ) === $saved_client_id ) + { + $current_client = $client_row; + break; + } + } + if ( !$current_client ) + { + $current_client = $clients[0]; + $saved_client_id = (int) ( $current_client['id'] ?? 0 ); + $saved_stage = 'campaigns'; + $saved_date_index = 0; + } + + if ( in_array( $saved_stage, [ 'campaigns', 'products' ], true ) ) + { + $saved_date_index = max( 0, min( $dates_total - 1, $saved_date_index ) ); + } + else + { + $saved_date_index = 0; + } + + $current_sync_date = in_array( $saved_stage, [ 'campaigns', 'products' ], true ) + ? (string) ( $sync_dates[ $saved_date_index ] ?? $sync_date ) + : $sync_date; + + $stage_result = self::run_clients_bundle_stage_for_client( $saved_stage, $current_client, $api, $current_sync_date ); + $errors = (array) ( $stage_result['errors'] ?? [] ); + $can_advance = empty( $errors ); + $forced_advance_due_errors = false; + $stage_retry_attempt = 0; + + $stage_signature = (int) ( $current_client['id'] ?? 0 ) . '|' . $saved_stage . '|' . ( in_array( $saved_stage, [ 'campaigns', 'products' ], true ) ? $current_sync_date : 'merchant' ); + $saved_retry_signature = trim( (string) self::get_setting_value( 'cron_clients_bundle_retry_signature', '' ) ); + $saved_retry_count = (int) self::get_setting_value( 'cron_clients_bundle_retry_count', 0 ); + + if ( !$can_advance ) + { + if ( $saved_retry_signature === $stage_signature ) + { + $stage_retry_attempt = $saved_retry_count + 1; + } + else + { + $stage_retry_attempt = 1; + } + + self::set_setting_value( 'cron_clients_bundle_retry_signature', $stage_signature ); + self::set_setting_value( 'cron_clients_bundle_retry_count', (string) $stage_retry_attempt ); + self::set_setting_value( 'cron_clients_bundle_last_error', implode( ' | ', array_slice( $errors, 0, 3 ) ) ); + + if ( $stage_retry_attempt >= $max_stage_retries ) + { + $can_advance = true; + $forced_advance_due_errors = true; + } + } + else + { + self::set_setting_value( 'cron_clients_bundle_retry_signature', '' ); + self::set_setting_value( 'cron_clients_bundle_retry_count', '0' ); + self::set_setting_value( 'cron_clients_bundle_last_error', '' ); + } + + $next_stage = $saved_stage; + $next_client_id = $saved_client_id; + $next_date_index = $saved_date_index; + $advanced = false; + $cycle_completed = false; + + if ( $can_advance ) + { + if ( $saved_stage === 'merchant' ) + { + // Dla wspolnego dashboardu/klientow aktualizujemy ten sam kursor co dedykowany cron Merchant. + self::set_setting_value( 'cron_campaigns_product_alerts_last_client_id', (string) $saved_client_id ); + } + + if ( $saved_stage === 'campaigns' ) + { + if ( $saved_date_index < ( $dates_total - 1 ) ) + { + $next_date_index = $saved_date_index + 1; + } + else + { + $next_stage = 'products'; + $next_date_index = 0; + } + } + else if ( $saved_stage === 'products' ) + { + if ( $saved_date_index < ( $dates_total - 1 ) ) + { + $next_date_index = $saved_date_index + 1; + } + else + { + $next_stage = 'merchant'; + $next_date_index = 0; + } + } + else + { + $next_stage = 'campaigns'; + $next_date_index = 0; + $next_client_id = self::get_next_client_id_in_order( $client_ids, $saved_client_id ); + $advanced = true; + + $last_client_id = (int) end( $client_ids ); + if ( $last_client_id > 0 && $saved_client_id === $last_client_id && $next_client_id === (int) ( $client_ids[0] ?? 0 ) ) + { + $cycle_completed = true; + self::set_setting_value( 'cron_clients_bundle_last_cycle_completed_at', date( 'Y-m-d H:i:s' ) ); + } + } + } + + self::set_setting_value( 'cron_clients_bundle_stage', $next_stage ); + self::set_setting_value( 'cron_clients_bundle_client_id', (string) $next_client_id ); + self::set_setting_value( 'cron_clients_bundle_date_index', (string) $next_date_index ); + + if ( $can_advance ) + { + self::set_setting_value( 'cron_clients_bundle_retry_signature', '' ); + self::set_setting_value( 'cron_clients_bundle_retry_count', '0' ); + } + + self::set_setting_value( 'cron_clients_bundle_last_processed_client_id', (string) (int) ( $current_client['id'] ?? 0 ) ); + self::set_setting_value( 'cron_clients_bundle_last_processed_stage', $saved_stage ); + self::set_setting_value( 'cron_clients_bundle_last_processed_date', (string) $current_sync_date ); + self::set_setting_value( 'cron_clients_bundle_last_processed_status', empty( $errors ) ? 'ok' : ( $forced_advance_due_errors ? 'forced_advance' : 'error' ) ); + self::set_setting_value( 'cron_clients_bundle_last_processed_at', date( 'Y-m-d H:i:s' ) ); + + $next_sync_date = in_array( $next_stage, [ 'campaigns', 'products' ], true ) + ? (string) ( $sync_dates[ $next_date_index ] ?? $sync_date ) + : $sync_date; + + echo json_encode( [ + 'result' => empty( $errors ) ? 'Cron zbiorczy: etap zakonczony.' : ( $forced_advance_due_errors ? 'Cron zbiorczy: przekroczono limit prob, przejscie do kolejnego etapu.' : 'Cron zbiorczy: etap zakonczony z bledami.' ), + 'date' => $sync_date, + 'today_date' => $today_date, + 'window_days' => $conversion_window_days, + 'dates_window' => $sync_dates, + 'current_client_id' => (int) ( $current_client['id'] ?? 0 ), + 'current_client_name' => (string) ( $current_client['name'] ?? '' ), + 'processed_stage' => $saved_stage, + 'processed_date_index' => $saved_date_index, + 'processed_sync_date' => $current_sync_date, + 'next_stage' => $next_stage, + 'next_client_id' => $next_client_id, + 'next_date_index' => $next_date_index, + 'next_sync_date' => $next_sync_date, + 'can_advance' => $can_advance ? 1 : 0, + 'stage_retry_attempt' => $stage_retry_attempt, + 'stage_max_retries' => $max_stage_retries, + 'forced_advance_due_errors' => $forced_advance_due_errors ? 1 : 0, + 'advanced_to_next_client' => $advanced ? 1 : 0, + 'cycle_completed' => $cycle_completed ? 1 : 0, + 'clients_total' => count( $client_ids ), + 'stage_details' => $stage_result, + 'errors' => $errors + ] ); + exit; + } + + static private function run_clients_bundle_stage_for_client( $stage, $client, $api, $sync_date ) + { + $stage = trim( (string) $stage ); + $sync_date = date( 'Y-m-d', strtotime( $sync_date ) ); + $client_id = (int) ( $client['id'] ?? 0 ); + + if ( $client_id <= 0 ) + { + return [ + 'stage' => $stage, + 'count' => 0, + 'errors' => [ 'Brak poprawnego klienta do przetworzenia.' ] + ]; + } + + if ( $stage === 'campaigns' ) + { + $phase = self::get_sync_phase_for_client_date( 'campaigns', $sync_date, $client_id ); + if ( $phase === 'done' ) + { + return [ + 'stage' => 'campaigns', + 'count' => 0, + 'meta' => [ + 'sync_date' => $sync_date, + 'already_done' => 1 + ], + 'errors' => [] + ]; + } + + $sync = self::sync_campaigns_for_client( $client, $api, $sync_date, true ); + $errors = (array) ( $sync['errors'] ?? [] ); + + if ( empty( $errors ) ) + { + self::mark_sync_phase( 'campaigns', $sync_date, $client_id, 'done' ); + } + else + { + self::mark_sync_phase( 'campaigns', $sync_date, $client_id, 'pending', implode( '; ', $errors ) ); + } + + return [ + 'stage' => 'campaigns', + 'count' => (int) ( $sync['processed_records'] ?? 0 ), + 'meta' => [ + 'sync_date' => $sync_date, + 'already_done' => 0, + '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' => $errors + ]; + } + + if ( $stage === 'products' ) + { + $phase = self::get_sync_phase_for_client_date( 'products', $sync_date, $client_id ); + if ( $phase === 'done' ) + { + return [ + 'stage' => 'products', + 'count' => 0, + 'meta' => [ + 'sync_date' => $sync_date, + 'already_done' => 1 + ], + 'errors' => [] + ]; + } + + $sync = self::sync_products_fetch_for_client( $client, $api, $sync_date ); + $history_30 = (int) self::aggregate_products_history_30_for_client( $client_id, $sync_date ); + $temp_rows = (int) self::rebuild_products_temp_for_client( $client_id ); + $errors = (array) ( $sync['errors'] ?? [] ); + + if ( empty( $errors ) ) + { + self::mark_sync_phase( 'products', $sync_date, $client_id, 'done' ); + } + else + { + self::mark_sync_phase( 'products', $sync_date, $client_id, 'pending', implode( '; ', $errors ) ); + } + + return [ + 'stage' => 'products', + 'count' => (int) ( $sync['processed_products'] ?? 0 ), + 'meta' => [ + 'sync_date' => $sync_date, + 'already_done' => 0, + 'skipped' => (int) ( $sync['skipped'] ?? 0 ), + 'history_30_products' => $history_30, + 'products_temp_rows' => $temp_rows + ], + 'errors' => $errors + ]; + } + + if ( $stage === 'merchant' ) + { + $merchant_account_id = trim( (string) ( $client['google_merchant_account_id'] ?? '' ) ); + if ( $merchant_account_id === '' ) + { + return [ + 'stage' => 'merchant', + 'count' => 0, + 'meta' => [ 'sync_date' => $sync_date, 'skipped' => 1, 'reason' => 'Klient nie ma Merchant Account ID.' ], + 'errors' => [] + ]; + } + + $campaigns_db_map = self::build_campaigns_db_map_for_client( $client_id ); + if ( empty( $campaigns_db_map ) ) + { + return [ + 'stage' => 'merchant', + 'count' => 0, + 'meta' => [ 'sync_date' => $sync_date, 'skipped' => 1, 'reason' => 'Brak kampanii klienta w bazie.' ], + 'errors' => [] + ]; + } + + $ad_group_db_map = self::build_ad_group_db_map_from_db( $campaigns_db_map ); + $sync = self::sync_product_campaign_alerts_for_client( + $client, + $campaigns_db_map, + $ad_group_db_map, + (string) ( $client['google_ads_customer_id'] ?? '' ), + $api, + $sync_date, + [ + 'run_missing_mapping_alerts' => false, + 'run_merchant_validation_alerts' => true + ] + ); + + return [ + 'stage' => 'merchant', + 'count' => (int) ( $sync['count'] ?? 0 ), + 'meta' => [ 'sync_date' => $sync_date ], + 'errors' => (array) ( $sync['errors'] ?? [] ) + ]; + } + + return [ + 'stage' => $stage, + 'count' => 0, + 'errors' => [ 'Nieznany etap crona zbiorczego: ' . $stage ] + ]; + } + + static private function get_sync_phase_for_client_date( $pipeline, $sync_date, $client_id ) + { + global $mdb; + + $row = $mdb -> get( 'cron_sync_status', [ 'phase' ], [ + 'AND' => [ + 'pipeline' => $pipeline, + 'sync_date' => $sync_date, + 'client_id' => (int) $client_id + ] + ] ); + + if ( !is_array( $row ) || !isset( $row['phase'] ) ) + { + return null; + } + + return (string) $row['phase']; + } + + static private function get_next_client_id_in_order( $client_ids, $current_client_id ) + { + $client_ids = array_values( array_filter( array_map( 'intval', (array) $client_ids ) ) ); + $current_client_id = (int) $current_client_id; + + if ( empty( $client_ids ) ) + { + return 0; + } + + $index = array_search( $current_client_id, $client_ids, true ); + if ( $index === false ) + { + return (int) $client_ids[0]; + } + + $next_index = ( $index + 1 ) % count( $client_ids ); + return (int) $client_ids[ $next_index ]; + } + + static private function sync_campaigns_snapshot_for_client( $client, $api, $as_of_date ) + { + global $mdb; + + $as_of_date = $as_of_date ? date( 'Y-m-d', strtotime( $as_of_date ) ) : date( 'Y-m-d' ); + $processed = 0; + $errors = []; + $customer_id = trim( (string) ( $client['google_ads_customer_id'] ?? '' ) ); + + if ( $customer_id === '' ) + { + return [ + 'processed_records' => 0, + 'ad_groups_synced' => 0, + 'errors' => [ 'Brak Google Ads Customer ID dla klienta ID ' . (int) ( $client['id'] ?? 0 ) . '.' ] + ]; + } + + $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' ); + return [ + 'processed_records' => 0, + 'ad_groups_synced' => 0, + 'errors' => [ 'Blad API kampanii (30 dni) dla klienta ' . (string) ( $client['name'] ?? '' ) . ': ' . $last_err ] + ]; + } + + $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' ); + return [ + 'processed_records' => 0, + 'ad_groups_synced' => 0, + 'errors' => [ 'Blad API kampanii (all time) dla klienta ' . (string) ( $client['name'] ?? '' ) . ': ' . $last_err ] + ]; + } + + if ( !is_array( $campaigns_30 ) ) + { + $campaigns_30 = []; + } + if ( !is_array( $campaigns_all_time ) ) + { + $campaigns_all_time = []; + } + + $all_time_map = []; + $all_time_totals = [ 'cost' => 0.0, 'conversion_value' => 0.0 ]; + foreach ( $campaigns_all_time as $cat ) + { + $cid = (string) ( $cat['campaign_id'] ?? '' ); + if ( $cid === '' ) + { + continue; + } + + $all_time_map[ $cid ] = [ + 'roas_all_time' => (float) ( $cat['roas_all_time'] ?? 0 ), + 'cost_all_time' => (float) ( $cat['cost_all_time'] ?? 0 ), + 'conversion_value_all_time' => (float) ( $cat['conversion_value_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 ]; + $campaigns_db_map = []; + + foreach ( $campaigns_30 as $campaign ) + { + $external_campaign_id = isset( $campaign['campaign_id'] ) ? (string) $campaign['campaign_id'] : ''; + if ( $external_campaign_id === '' ) + { + continue; + } + + $campaign_name = trim( (string) ( $campaign['campaign_name'] ?? '' ) ); + if ( $campaign_name === '' ) + { + $campaign_name = 'Campaign #' . $external_campaign_id; + } + + $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 ); + + $db_campaign_id = (int) $mdb -> get( 'campaigns', 'id', [ 'AND' => [ + 'client_id' => (int) $client['id'], + 'campaign_id' => $external_campaign_id + ] ] ); + + if ( $db_campaign_id > 0 ) + { + $mdb -> update( 'campaigns', [ + 'campaign_name' => $campaign_name, + 'advertising_channel_type' => $advertising_channel_type !== '' ? $advertising_channel_type : null + ], [ 'id' => $db_campaign_id ] ); + } + else + { + $mdb -> insert( 'campaigns', [ + 'client_id' => (int) $client['id'], + 'campaign_id' => $external_campaign_id, + 'campaign_name' => $campaign_name, + 'advertising_channel_type' => $advertising_channel_type !== '' ? $advertising_channel_type : null + ] ); + $db_campaign_id = (int) $mdb -> id(); + } + + if ( $db_campaign_id <= 0 ) + { + continue; + } + + $all_time_for_campaign = $all_time_map[ $external_campaign_id ] ?? [ 'roas_all_time' => 0 ]; + $bidding_strategy = self::format_bidding_strategy( + $campaign['bidding_strategy'] ?? '', + $campaign['target_roas'] ?? 0 + ); + + $history_data = [ + 'roas_30_days' => (float) ( $campaign['roas_30_days'] ?? 0 ), + 'roas_all_time' => (float) ( $all_time_for_campaign['roas_all_time'] ?? 0 ), + 'budget' => (float) ( $campaign['budget'] ?? 0 ), + 'money_spent' => (float) ( $campaign['money_spent'] ?? 0 ), + 'conversion_value' => (float) ( $campaign['conversion_value'] ?? 0 ), + '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 ] = $db_campaign_id; + $processed++; + } + + $db_account_campaign_id = (int) $mdb -> get( 'campaigns', 'id', [ 'AND' => [ + 'client_id' => (int) $client['id'], + 'campaign_id' => 0 + ] ] ); + + if ( $db_account_campaign_id > 0 ) + { + $mdb -> update( 'campaigns', [ + 'campaign_name' => '--- konto ---', + 'advertising_channel_type' => null + ], [ 'id' => $db_account_campaign_id ] ); + } + else + { + $mdb -> insert( 'campaigns', [ + 'client_id' => (int) $client['id'], + 'campaign_id' => 0, + 'campaign_name' => '--- konto ---', + 'advertising_channel_type' => null + ] ); + $db_account_campaign_id = (int) $mdb -> id(); + } + + if ( $db_account_campaign_id > 0 ) + { + $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; + + $account_history_data = [ + 'roas_30_days' => $account_roas_30, + 'roas_all_time' => $account_roas_all_time, + 'budget' => (float) $account_30_totals['budget'], + 'money_spent' => (float) $account_30_totals['money_spent'], + 'conversion_value' => (float) $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++; + } + + // Grupy reklam / grupy plikow PMAX zapisujemy do campaign_ad_groups. + $ad_groups_sync = self::sync_campaign_ad_groups_for_client( $campaigns_db_map, $customer_id, $api, $as_of_date, $as_of_date ); + $errors = array_merge( $errors, (array) ( $ad_groups_sync['errors'] ?? [] ) ); + + return [ + 'processed_records' => $processed, + 'ad_groups_synced' => (int) ( $ad_groups_sync['count'] ?? 0 ), + 'errors' => $errors + ]; + } + + static private function sync_campaign_terms_backfill_for_client( $client_id, $customer_id, $api, $sync_dates ) + { + $client_id = (int) $client_id; + $customer_id = trim( (string) $customer_id ); + $sync_dates = array_values( array_filter( array_map( function( $date_item ) + { + $ts = strtotime( (string) $date_item ); + return $ts ? date( 'Y-m-d', $ts ) : null; + }, (array) $sync_dates ) ) ); + + if ( $client_id <= 0 || $customer_id === '' || empty( $sync_dates ) ) + { + return [ 'history_synced' => 0, 'aggregated' => 0, 'errors' => [] ]; + } + + $campaigns_db_map = self::build_campaigns_db_map_for_client( $client_id ); + if ( empty( $campaigns_db_map ) ) + { + return [ 'history_synced' => 0, 'aggregated' => 0, 'errors' => [] ]; + } + + $ad_group_db_map = self::build_ad_group_db_map_from_db( $campaigns_db_map ); + + $history_synced = 0; + $errors = []; + + foreach ( $sync_dates as $sync_date_item ) + { + $daily = self::sync_campaign_search_terms_daily( $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $sync_date_item ); + $history_synced += (int) ( $daily['count'] ?? 0 ); + if ( !empty( $daily['errors'] ) ) + { + $errors = array_merge( $errors, (array) $daily['errors'] ); + } + } + + $aggregate_date = end( $sync_dates ); + if ( !$aggregate_date ) + { + $aggregate_date = date( 'Y-m-d' ); + } + + $aggregated = self::aggregate_campaign_search_terms_for_client( $client_id, $aggregate_date ); + + return [ + 'history_synced' => (int) $history_synced, + 'aggregated' => (int) $aggregated, + 'errors' => $errors + ]; + } + + static private function sync_campaign_keywords_and_negatives_for_client( $client_id, $customer_id, $api, $date_sync ) + { + $client_id = (int) $client_id; + $customer_id = trim( (string) $customer_id ); + $date_sync = $date_sync ? date( 'Y-m-d', strtotime( $date_sync ) ) : date( 'Y-m-d' ); + + if ( $client_id <= 0 || $customer_id === '' ) + { + return [ 'keywords_synced' => 0, 'negative_keywords_synced' => 0, 'errors' => [] ]; + } + + $campaigns_db_map = self::build_campaigns_db_map_for_client( $client_id ); + if ( empty( $campaigns_db_map ) ) + { + return [ 'keywords_synced' => 0, 'negative_keywords_synced' => 0, 'errors' => [] ]; + } + + $ad_group_db_map = self::build_ad_group_db_map_from_db( $campaigns_db_map ); + + $keywords_sync = self::sync_campaign_keywords_for_client( $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $date_sync ); + $negative_sync = self::sync_campaign_negative_keywords_for_client( $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $date_sync ); + + return [ + 'keywords_synced' => (int) ( $keywords_sync['count'] ?? 0 ), + 'negative_keywords_synced' => (int) ( $negative_sync['count'] ?? 0 ), + 'errors' => array_merge( + (array) ( $keywords_sync['errors'] ?? [] ), + (array) ( $negative_sync['errors'] ?? [] ) + ) + ]; + } + + 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; @@ -2350,7 +4341,18 @@ class Cron $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 ); + $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'] ); @@ -2365,13 +4367,36 @@ class Cron ]; } - static private function sync_product_campaign_alerts_for_client( $client, $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $date_sync ) + static private function sync_product_campaign_alerts_for_client( $client, $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $date_sync, $options = [] ) { global $mdb; + $run_missing_mapping_alerts = array_key_exists( 'run_missing_mapping_alerts', (array) $options ) + ? (bool) $options['run_missing_mapping_alerts'] + : true; + $run_merchant_validation_alerts = array_key_exists( 'run_merchant_validation_alerts', (array) $options ) + ? (bool) $options['run_merchant_validation_alerts'] + : true; + + if ( !$run_missing_mapping_alerts && !$run_merchant_validation_alerts ) + { + return [ 'count' => 0, 'errors' => [] ]; + } + + self::log_product_alerts_debug( 'sync_product_campaign_alerts_for_client:start', [ + 'client_id' => (int) ( $client['id'] ?? 0 ), + 'customer_id' => (string) $customer_id, + 'date_sync' => (string) $date_sync, + 'campaigns_db_map_count' => count( (array) $campaigns_db_map ), + 'ad_group_db_map_count' => count( (array) $ad_group_db_map ), + 'run_missing_mapping_alerts' => $run_missing_mapping_alerts ? 1 : 0, + 'run_merchant_validation_alerts' => $run_merchant_validation_alerts ? 1 : 0 + ] ); + $campaign_db_ids = array_values( array_unique( array_map( 'intval', array_values( $campaigns_db_map ) ) ) ); if ( empty( $campaign_db_ids ) ) { + self::log_product_alerts_debug( 'sync_product_campaign_alerts_for_client:skip_no_campaigns', [] ); return [ 'count' => 0, 'errors' => [] ]; } @@ -2380,8 +4405,11 @@ class Cron $client_name = trim( (string) ( $client['name'] ?? '' ) ); $merchant_account_id = preg_replace( '/\D+/', '', (string) ( $client['google_merchant_account_id'] ?? '' ) ); - if ( $merchant_account_id === '' ) + if ( $run_merchant_validation_alerts && $merchant_account_id === '' ) { + self::log_product_alerts_debug( 'sync_product_campaign_alerts_for_client:skip_no_merchant_account', [ + 'client_id' => $client_id + ] ); return [ 'count' => 0, 'errors' => [] ]; } @@ -2394,6 +4422,10 @@ class Cron if ( !is_array( $shopping_campaigns ) || empty( $shopping_campaigns ) ) { + self::log_product_alerts_debug( 'sync_product_campaign_alerts_for_client:skip_no_shopping_campaigns', [ + 'client_id' => $client_id, + 'campaign_db_ids' => $campaign_db_ids + ] ); return [ 'count' => 0, 'errors' => [] ]; } @@ -2416,6 +4448,7 @@ class Cron if ( empty( $shopping_campaign_external_ids ) ) { + self::log_product_alerts_debug( 'sync_product_campaign_alerts_for_client:skip_no_external_campaign_ids', [] ); return [ 'count' => 0, 'errors' => [] ]; } @@ -2449,19 +4482,77 @@ class Cron $shopping_ad_groups_by_scope[ $scope_key ] = $ag_row; } - $ad_groups_offer_ids = $api -> get_shopping_ad_group_offer_ids( $customer_id ); - if ( $ad_groups_offer_ids === false ) + $ad_groups_offer_ids = []; + $offer_mapping_sources = []; + $missing_mapping_source_reliable = false; + + $ad_groups_offer_ids_listing = $api -> get_shopping_ad_group_offer_ids( $customer_id ); + self::log_product_alerts_debug( 'google_ads:get_shopping_ad_group_offer_ids', [ + 'result_type' => is_array( $ad_groups_offer_ids_listing ) ? 'array' : ( $ad_groups_offer_ids_listing === false ? 'false' : gettype( $ad_groups_offer_ids_listing ) ), + 'rows_count' => is_array( $ad_groups_offer_ids_listing ) ? count( $ad_groups_offer_ids_listing ) : 0, + 'sample' => is_array( $ad_groups_offer_ids_listing ) ? array_slice( $ad_groups_offer_ids_listing, 0, 3 ) : [] + ] ); + + if ( is_array( $ad_groups_offer_ids_listing ) ) { - $ad_groups_offer_ids = $api -> get_shopping_ad_group_offer_ids_from_performance( $customer_id ); + $ad_groups_offer_ids = self::merge_shopping_ad_group_offer_rows( $ad_groups_offer_ids, $ad_groups_offer_ids_listing ); + $offer_mapping_sources['listing_group'] = true; + $missing_mapping_source_reliable = true; } - if ( $ad_groups_offer_ids === false ) + $ad_groups_offer_ids_shopping_product = $api -> get_shopping_ad_group_offer_ids_from_shopping_product( $customer_id ); + self::log_product_alerts_debug( 'google_ads:get_shopping_ad_group_offer_ids_from_shopping_product', [ + 'result_type' => is_array( $ad_groups_offer_ids_shopping_product ) ? 'array' : ( $ad_groups_offer_ids_shopping_product === false ? 'false' : gettype( $ad_groups_offer_ids_shopping_product ) ), + 'rows_count' => is_array( $ad_groups_offer_ids_shopping_product ) ? count( $ad_groups_offer_ids_shopping_product ) : 0, + 'sample' => is_array( $ad_groups_offer_ids_shopping_product ) ? array_slice( $ad_groups_offer_ids_shopping_product, 0, 3 ) : [] + ] ); + + if ( is_array( $ad_groups_offer_ids_shopping_product ) ) { - $ad_groups_offer_ids = self::get_shopping_ad_group_offer_ids_from_history( $client_id, array_keys( $shopping_campaign_names_by_db_id ) ); + $ad_groups_offer_ids = self::merge_shopping_ad_group_offer_rows( $ad_groups_offer_ids, $ad_groups_offer_ids_shopping_product ); + $offer_mapping_sources['shopping_product'] = true; } + if ( empty( $ad_groups_offer_ids ) ) + { + $ad_groups_offer_ids_performance = $api -> get_shopping_ad_group_offer_ids_from_performance( $customer_id ); + self::log_product_alerts_debug( 'google_ads:get_shopping_ad_group_offer_ids_from_performance', [ + 'result_type' => is_array( $ad_groups_offer_ids_performance ) ? 'array' : ( $ad_groups_offer_ids_performance === false ? 'false' : gettype( $ad_groups_offer_ids_performance ) ), + 'rows_count' => is_array( $ad_groups_offer_ids_performance ) ? count( $ad_groups_offer_ids_performance ) : 0, + 'sample' => is_array( $ad_groups_offer_ids_performance ) ? array_slice( $ad_groups_offer_ids_performance, 0, 3 ) : [] + ] ); + + if ( is_array( $ad_groups_offer_ids_performance ) ) + { + $ad_groups_offer_ids = self::merge_shopping_ad_group_offer_rows( $ad_groups_offer_ids, $ad_groups_offer_ids_performance ); + $offer_mapping_sources['performance'] = true; + } + } + + if ( empty( $ad_groups_offer_ids ) ) + { + $ad_groups_offer_ids_history = self::get_shopping_ad_group_offer_ids_from_history( $client_id, array_keys( $shopping_campaign_names_by_db_id ) ); + self::log_product_alerts_debug( 'db:get_shopping_ad_group_offer_ids_from_history', [ + 'rows_count' => is_array( $ad_groups_offer_ids_history ) ? count( $ad_groups_offer_ids_history ) : 0, + 'sample' => is_array( $ad_groups_offer_ids_history ) ? array_slice( $ad_groups_offer_ids_history, 0, 3 ) : [] + ] ); + + if ( is_array( $ad_groups_offer_ids_history ) ) + { + $ad_groups_offer_ids = self::merge_shopping_ad_group_offer_rows( $ad_groups_offer_ids, $ad_groups_offer_ids_history ); + $offer_mapping_sources['history'] = true; + } + } + + self::log_product_alerts_debug( 'google_ads:offer_id_sources_summary', [ + 'sources' => array_values( array_keys( $offer_mapping_sources ) ), + 'rows_count' => count( (array) $ad_groups_offer_ids ), + 'missing_mapping_source_reliable' => $missing_mapping_source_reliable ? 1 : 0 + ] ); + if ( !is_array( $ad_groups_offer_ids ) || empty( $ad_groups_offer_ids ) ) { + self::log_product_alerts_debug( 'sync_product_campaign_alerts_for_client:skip_no_ad_group_offer_rows', [] ); return [ 'count' => 0, 'errors' => [] ]; } @@ -2474,7 +4565,7 @@ class Cron $ad_group_external_id = (string) ( (int) ( $row['ad_group_id'] ?? 0 ) ); $offer_ids = array_values( array_unique( array_filter( array_map( function( $item ) { - return trim( (string) $item ); + return self::normalize_offer_id_for_lookup( $item ); }, (array) ( $row['offer_ids'] ?? [] ) ) ) ) ); if ( $campaign_external_id === '0' || $ad_group_external_id === '0' || empty( $offer_ids ) ) @@ -2496,28 +4587,116 @@ class Cron 'offer_ids' => $offer_ids ]; + if ( !$run_merchant_validation_alerts ) + { + continue; + } + foreach ( $offer_ids as $offer_id ) { - $offer_ids_to_verify[ $offer_id ] = true; + foreach ( self::build_offer_id_lookup_variants( $offer_id ) as $offer_id_variant ) + { + $offer_ids_to_verify[ $offer_id_variant ] = true; + } } } + if ( $run_merchant_validation_alerts ) + { + self::log_product_alerts_debug( 'merchant:offer_ids_to_verify', [ + 'count' => count( $offer_ids_to_verify ), + 'sample' => array_slice( array_values( array_keys( $offer_ids_to_verify ) ), 0, 30 ) + ] ); + } + $merchant_items_map = []; - if ( !empty( $offer_ids_to_verify ) ) + if ( $run_merchant_validation_alerts && !empty( $offer_ids_to_verify ) ) { $merchant_items_map = $api -> get_merchant_products_for_offer_ids( $merchant_account_id, array_keys( $offer_ids_to_verify ) ); + self::log_product_alerts_debug( 'merchant:get_merchant_products_for_offer_ids', [ + 'merchant_account_id' => $merchant_account_id, + 'request_count' => count( $offer_ids_to_verify ), + 'response_type' => is_array( $merchant_items_map ) ? 'array' : ( $merchant_items_map === false ? 'false' : gettype( $merchant_items_map ) ), + 'response_count' => is_array( $merchant_items_map ) ? count( $merchant_items_map ) : 0, + 'response_sample_keys' => is_array( $merchant_items_map ) ? array_slice( array_values( array_keys( $merchant_items_map ) ), 0, 30 ) : [] + ] ); + if ( $merchant_items_map === false ) { - $merchant_items_map = []; + $merchant_error = trim( (string) \services\GoogleAdsApi::get_setting( 'google_merchant_last_error' ) ); + + if ( $merchant_error === '' ) + { + $merchant_error = 'Nie udalo sie pobrac produktow z Merchant Center.'; + } + + return [ + 'count' => 0, + 'errors' => [ 'Pominieto alerty produktowe: ' . $merchant_error ] + ]; } } - if ( !is_array( $merchant_items_map ) ) + if ( $run_merchant_validation_alerts && !is_array( $merchant_items_map ) ) { $merchant_items_map = []; } + $local_offer_ids_map = []; + if ( $run_merchant_validation_alerts ) + { + $local_offer_ids_rows = $mdb -> query( + "SELECT offer_id + FROM products + WHERE client_id = :client_id + AND TRIM( COALESCE( offer_id, '' ) ) <> ''", + [ ':client_id' => $client_id ] + ) -> fetchAll( \PDO::FETCH_ASSOC ); + + foreach ( (array) $local_offer_ids_rows as $local_row ) + { + $local_offer_norm = self::normalize_offer_id_for_compare( $local_row['offer_id'] ?? '' ); + if ( $local_offer_norm !== '' ) + { + $local_offer_ids_map[ $local_offer_norm ] = true; + } + } + } + + if ( $run_merchant_validation_alerts ) + { + self::log_product_alerts_debug( 'db:local_offer_ids', [ + 'count' => count( $local_offer_ids_map ), + 'sample' => array_slice( array_values( array_keys( $local_offer_ids_map ) ), 0, 30 ) + ] ); + } + + $merchant_items_map_normalized = []; + if ( $run_merchant_validation_alerts ) + { + foreach ( $merchant_items_map as $merchant_offer_id => $merchant_item ) + { + $offer_id_norm = self::normalize_offer_id_for_compare( $merchant_offer_id ); + if ( $offer_id_norm === '' || isset( $merchant_items_map_normalized[ $offer_id_norm ] ) ) + { + continue; + } + + $merchant_items_map_normalized[ $offer_id_norm ] = $merchant_item; + } + } + $inserted = 0; + $problematic_scopes_by_type = []; + if ( $run_merchant_validation_alerts ) + { + $problematic_scopes_by_type['ad_group_without_active_product'] = []; + $problematic_scopes_by_type['ad_group_with_orphaned_offers'] = []; + } + if ( $run_missing_mapping_alerts ) + { + $problematic_scopes_by_type['ad_group_without_detected_product'] = []; + } $insert_alert = function( $alert_type, $campaign_external_id, $ad_group_external_id, $db_campaign_id, $db_ad_group_id, $message, $meta ) use ( $mdb, $client_id, $date_sync ) { @@ -2552,8 +4731,10 @@ class Cron return true; }; - foreach ( $candidate_rows as $scope_key => $row ) + if ( $run_merchant_validation_alerts ) { + foreach ( $candidate_rows as $scope_key => $row ) + { $campaign_external_id = (int) $row['campaign_external_id']; $ad_group_external_id = (int) $row['ad_group_external_id']; $offer_ids = (array) $row['offer_ids']; @@ -2565,9 +4746,22 @@ class Cron $active_offer_count = 0; $orphaned_offer_ids = []; + $local_known_offer_count = 0; foreach ( $offer_ids as $offer_id ) { - if ( isset( $merchant_items_map[ $offer_id ] ) ) + $matched = false; + foreach ( self::build_offer_id_lookup_variants( $offer_id ) as $offer_id_variant ) + { + if ( isset( $merchant_items_map[ $offer_id_variant ] ) ) + { + $matched = true; + break; + } + } + + $offer_id_norm = self::normalize_offer_id_for_compare( $offer_id ); + + if ( $matched || ( $offer_id_norm !== '' && isset( $merchant_items_map_normalized[ $offer_id_norm ] ) ) ) { $active_offer_count++; } @@ -2575,8 +4769,23 @@ class Cron { $orphaned_offer_ids[] = $offer_id; } + + if ( $offer_id_norm !== '' && isset( $local_offer_ids_map[ $offer_id_norm ] ) ) + { + $local_known_offer_count++; + } } + self::log_product_alerts_debug( 'scope:verification', [ + 'scope_key' => $scope_key, + 'campaign_external_id' => $campaign_external_id, + 'ad_group_external_id' => $ad_group_external_id, + 'offer_ids' => $offer_ids, + 'active_offer_count' => $active_offer_count, + 'orphaned_offer_ids' => $orphaned_offer_ids, + 'local_known_offer_count' => $local_known_offer_count + ] ); + if ( $active_offer_count > 0 && empty( $orphaned_offer_ids ) ) { continue; @@ -2603,6 +4812,17 @@ class Cron if ( $active_offer_count === 0 ) { + if ( $local_known_offer_count > 0 ) + { + self::log_product_alerts_debug( 'scope:skip_alert_local_offer_match', [ + 'scope_key' => $scope_key, + 'campaign_external_id' => $campaign_external_id, + 'ad_group_external_id' => $ad_group_external_id, + 'local_known_offer_count' => $local_known_offer_count + ] ); + continue; + } + $message = 'Brak aktywnych produktów w Merchant Center. Grupa reklam to "' . $ad_group_name . '" w kampanii "' . $campaign_name . '" na koncie klienta "' . $client_name . '".'; if ( $insert_alert( @@ -2612,15 +4832,17 @@ class Cron $db_campaign_id, $db_ad_group_id, $message, - [ - 'offer_ids' => $offer_ids, - 'merchant_account_id' => $merchant_account_id, - 'source' => 'cron_campaigns_sync' - ] + [ + 'offer_ids' => $offer_ids, + 'merchant_account_id' => $merchant_account_id, + 'source' => 'cron_campaigns_sync_merchant' + ] ) ) { $inserted++; } + + $problematic_scopes_by_type['ad_group_without_active_product'][ $scope_key ] = true; } if ( !empty( $orphaned_offer_ids ) && $active_offer_count > 0 ) @@ -2641,75 +4863,338 @@ class Cron $db_ad_group_id, $message, [ - 'orphaned_offer_ids' => $orphaned_offer_ids, - 'active_offer_count' => $active_offer_count, - 'total_offer_count' => count( $offer_ids ), + 'orphaned_offer_ids' => $orphaned_offer_ids, + 'active_offer_count' => $active_offer_count, + 'total_offer_count' => count( $offer_ids ), + 'merchant_account_id' => $merchant_account_id, + 'source' => 'cron_campaigns_sync_merchant' + ] + ) ) + { + $inserted++; + } + + $problematic_scopes_by_type['ad_group_with_orphaned_offers'][ $scope_key ] = true; + } + } + } + + if ( $run_missing_mapping_alerts && $missing_mapping_source_reliable ) + { + foreach ( $shopping_ad_groups_by_scope as $scope_key => $ag_row ) + { + if ( isset( $candidate_rows[ $scope_key ] ) ) + { + continue; + } + + $campaign_external_id = (int) ( $ag_row['campaign_external_id'] ?? 0 ); + $ad_group_external_id = (int) ( $ag_row['ad_group_external_id'] ?? 0 ); + $db_campaign_id = (int) ( $ag_row['campaign_db_id'] ?? 0 ); + $db_ad_group_id = (int) ( $ag_row['ad_group_db_id'] ?? 0 ); + + if ( $campaign_external_id <= 0 || $ad_group_external_id <= 0 ) + { + continue; + } + + $campaign_name = trim( (string) ( $ag_row['campaign_name'] ?? '' ) ); + if ( $campaign_name === '' ) + { + $campaign_name = 'Kampania #' . $campaign_external_id; + } + + $ad_group_name = trim( (string) ( $ag_row['ad_group_name'] ?? '' ) ); + if ( $ad_group_name === '' ) + { + $ad_group_name = 'Grupa reklam #' . $ad_group_external_id; + } + + $message = 'Brak wykrytego przypisanego produktu. Grupa reklam to "' . $ad_group_name . '" w kampanii "' . $campaign_name . '" na koncie klienta "' . $client_name . '".'; + + if ( $insert_alert( + 'ad_group_without_detected_product', + $campaign_external_id, + $ad_group_external_id, + $db_campaign_id, + $db_ad_group_id, + $message, + [ 'merchant_account_id' => $merchant_account_id, - 'source' => 'cron_campaigns_sync' + 'clicks_30' => (int) ( $ag_row['clicks_30'] ?? 0 ), + 'clicks_all_time' => (int) ( $ag_row['clicks_all_time'] ?? 0 ), + 'offer_mapping_sources' => array_values( array_keys( $offer_mapping_sources ) ), + 'source' => 'cron_campaigns_sync_missing_mapping' ] ) ) { $inserted++; } + + $problematic_scopes_by_type['ad_group_without_detected_product'][ $scope_key ] = true; } } + else if ( $run_missing_mapping_alerts ) + { + self::log_product_alerts_debug( 'scope:skip_without_detected_product_unreliable_source', [ + 'sources' => array_values( array_keys( $offer_mapping_sources ) ), + 'missing_mapping_source_reliable' => $missing_mapping_source_reliable ? 1 : 0 + ] ); + } + // Czysci alerty, ktore byly prawdziwe wczesniej, ale w aktualnym syncu + // nie sa juz problematyczne albo grupa nie jest juz aktywna/pobrana. + $active_scope_map = []; foreach ( $shopping_ad_groups_by_scope as $scope_key => $ag_row ) { - if ( isset( $candidate_rows[ $scope_key ] ) ) - { - continue; - } + $active_scope_map[ $scope_key ] = true; + } - $campaign_external_id = (int) ( $ag_row['campaign_external_id'] ?? 0 ); - $ad_group_external_id = (int) ( $ag_row['ad_group_external_id'] ?? 0 ); - $db_campaign_id = (int) ( $ag_row['campaign_db_id'] ?? 0 ); - $db_ad_group_id = (int) ( $ag_row['ad_group_db_id'] ?? 0 ); - - if ( $campaign_external_id <= 0 || $ad_group_external_id <= 0 ) - { - continue; - } - - $campaign_name = trim( (string) ( $ag_row['campaign_name'] ?? '' ) ); - if ( $campaign_name === '' ) - { - $campaign_name = 'Kampania #' . $campaign_external_id; - } - - $ad_group_name = trim( (string) ( $ag_row['ad_group_name'] ?? '' ) ); - if ( $ad_group_name === '' ) - { - $ad_group_name = 'Grupa reklam #' . $ad_group_external_id; - } - - $message = 'Brak wykrytego przypisanego produktu. Grupa reklam to "' . $ad_group_name . '" w kampanii "' . $campaign_name . '" na koncie klienta "' . $client_name . '".'; - - if ( $insert_alert( - 'ad_group_without_detected_product', - $campaign_external_id, - $ad_group_external_id, - $db_campaign_id, - $db_ad_group_id, - $message, - [ - 'merchant_account_id' => $merchant_account_id, - 'clicks_30' => (int) ( $ag_row['clicks_30'] ?? 0 ), - 'clicks_all_time' => (int) ( $ag_row['clicks_all_time'] ?? 0 ), - 'source' => 'cron_campaigns_sync_missing_mapping' + $alert_types_for_cleanup = array_keys( $problematic_scopes_by_type ); + $existing_product_alerts = []; + if ( !empty( $alert_types_for_cleanup ) ) + { + $existing_product_alerts = $mdb -> select( 'campaign_alerts', [ + 'id', + 'campaign_external_id', + 'ad_group_external_id', + 'alert_type' + ], [ + 'AND' => [ + 'client_id' => $client_id, + 'alert_type' => $alert_types_for_cleanup ] - ) ) + ] ); + } + + $cleaned = 0; + foreach ( (array) $existing_product_alerts as $existing_alert ) + { + $alert_id = (int) ( $existing_alert['id'] ?? 0 ); + if ( $alert_id <= 0 ) { - $inserted++; + continue; + } + + $alert_type = (string) ( $existing_alert['alert_type'] ?? '' ); + if ( $alert_type === '' || !isset( $problematic_scopes_by_type[ $alert_type ] ) ) + { + continue; + } + + $scope_key = (string) ( (int) ( $existing_alert['campaign_external_id'] ?? 0 ) ) . '|' . (string) ( (int) ( $existing_alert['ad_group_external_id'] ?? 0 ) ); + + if ( !isset( $active_scope_map[ $scope_key ] ) ) + { + $mdb -> delete( 'campaign_alerts', [ 'id' => $alert_id ] ); + $cleaned++; + continue; + } + + if ( !isset( $problematic_scopes_by_type[ $alert_type ][ $scope_key ] ) ) + { + $mdb -> delete( 'campaign_alerts', [ 'id' => $alert_id ] ); + $cleaned++; } } + self::log_product_alerts_debug( 'alerts:cleanup', [ + 'existing_count' => count( (array) $existing_product_alerts ), + 'cleaned_count' => $cleaned, + 'run_missing_mapping_alerts' => $run_missing_mapping_alerts ? 1 : 0, + 'run_merchant_validation_alerts' => $run_merchant_validation_alerts ? 1 : 0, + 'problematic_without_active_product' => count( (array) ( $problematic_scopes_by_type['ad_group_without_active_product'] ?? [] ) ), + 'problematic_with_orphaned_offers' => count( (array) ( $problematic_scopes_by_type['ad_group_with_orphaned_offers'] ?? [] ) ), + 'problematic_without_detected_product' => count( (array) ( $problematic_scopes_by_type['ad_group_without_detected_product'] ?? [] ) ) + ] ); + return [ 'count' => $inserted, 'errors' => [] ]; } + static private function log_product_alerts_debug( $stage, $context = [] ) + { + $stage = trim( (string) $stage ); + if ( $stage === '' ) + { + $stage = 'unknown_stage'; + } + + $log_file = dirname( __DIR__, 2 ) . '/tmp/campaign_alerts_debug.log'; + + $payload = [ + 'ts' => date( 'Y-m-d H:i:s' ), + 'stage' => $stage, + 'context' => is_array( $context ) ? $context : [ 'value' => $context ] + ]; + + $json = json_encode( $payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); + if ( $json === false ) + { + $json = '{"ts":"' . date( 'Y-m-d H:i:s' ) . '","stage":"json_encode_failed"}'; + } + + if ( strlen( $json ) > 30000 ) + { + $json = substr( $json, 0, 30000 ) . '...TRUNCATED'; + } + + @file_put_contents( $log_file, $json . PHP_EOL, FILE_APPEND | LOCK_EX ); + } + + static private function merge_shopping_ad_group_offer_rows( $base_rows, $rows_to_merge ) + { + $scope_map = []; + + foreach ( (array) $base_rows as $row ) + { + $campaign_id = (int) ( $row['campaign_id'] ?? 0 ); + $ad_group_id = (int) ( $row['ad_group_id'] ?? 0 ); + if ( $campaign_id <= 0 || $ad_group_id <= 0 ) + { + continue; + } + + $scope_key = $campaign_id . '|' . $ad_group_id; + $scope_map[ $scope_key ] = [ + 'campaign_id' => $campaign_id, + 'campaign_name' => trim( (string) ( $row['campaign_name'] ?? '' ) ), + 'ad_group_id' => $ad_group_id, + 'ad_group_name' => trim( (string) ( $row['ad_group_name'] ?? '' ) ), + 'offer_ids' => [] + ]; + + foreach ( (array) ( $row['offer_ids'] ?? [] ) as $offer_id ) + { + $offer_id_norm = self::normalize_offer_id_for_lookup( $offer_id ); + if ( $offer_id_norm === '' ) + { + continue; + } + $scope_map[ $scope_key ]['offer_ids'][ $offer_id_norm ] = true; + } + } + + foreach ( (array) $rows_to_merge as $row ) + { + $campaign_id = (int) ( $row['campaign_id'] ?? 0 ); + $ad_group_id = (int) ( $row['ad_group_id'] ?? 0 ); + if ( $campaign_id <= 0 || $ad_group_id <= 0 ) + { + continue; + } + + $scope_key = $campaign_id . '|' . $ad_group_id; + if ( !isset( $scope_map[ $scope_key ] ) ) + { + $scope_map[ $scope_key ] = [ + 'campaign_id' => $campaign_id, + 'campaign_name' => trim( (string) ( $row['campaign_name'] ?? '' ) ), + 'ad_group_id' => $ad_group_id, + 'ad_group_name' => trim( (string) ( $row['ad_group_name'] ?? '' ) ), + 'offer_ids' => [] + ]; + } + + if ( trim( (string) $scope_map[ $scope_key ]['campaign_name'] ) === '' ) + { + $scope_map[ $scope_key ]['campaign_name'] = trim( (string) ( $row['campaign_name'] ?? '' ) ); + } + if ( trim( (string) $scope_map[ $scope_key ]['ad_group_name'] ) === '' ) + { + $scope_map[ $scope_key ]['ad_group_name'] = trim( (string) ( $row['ad_group_name'] ?? '' ) ); + } + + foreach ( (array) ( $row['offer_ids'] ?? [] ) as $offer_id ) + { + $offer_id_norm = self::normalize_offer_id_for_lookup( $offer_id ); + if ( $offer_id_norm === '' ) + { + continue; + } + $scope_map[ $scope_key ]['offer_ids'][ $offer_id_norm ] = true; + } + } + + $rows = []; + foreach ( $scope_map as $scope_row ) + { + $offer_ids = array_values( array_keys( (array) ( $scope_row['offer_ids'] ?? [] ) ) ); + if ( empty( $offer_ids ) ) + { + continue; + } + + $scope_row['offer_ids'] = $offer_ids; + $rows[] = $scope_row; + } + + return $rows; + } + + static private function normalize_offer_id_for_lookup( $offer_id ) + { + $offer_id = trim( (string) $offer_id ); + + if ( $offer_id === '' ) + { + return ''; + } + + $offer_id = trim( $offer_id, " \t\n\r\0\x0B'\"" ); + + // Google Ads moze zwracac format channel:language:country:offer_id. + // W Merchant API do products.list kluczem jest offer_id. + if ( preg_match( '/^[a-z_]+:[a-z]{2,8}:[a-z]{2,8}:(.+)$/i', $offer_id, $matches ) ) + { + $offer_id = trim( (string) ( $matches[1] ?? '' ) ); + } + + return $offer_id; + } + + static private function normalize_offer_id_for_compare( $offer_id ) + { + $offer_id = self::normalize_offer_id_for_lookup( $offer_id ); + + if ( $offer_id === '' ) + { + return ''; + } + + $offer_id = strtolower( $offer_id ); + + return $offer_id; + } + + static private function build_offer_id_lookup_variants( $offer_id ) + { + $offer_id = self::normalize_offer_id_for_lookup( $offer_id ); + if ( $offer_id === '' ) + { + return []; + } + + $variants = [ $offer_id ]; + + // Shopify: spotykane roznice zapisu kraju w prefiksie (pl vs PL). + if ( preg_match( '/^shopify_([a-z]{2})_(.+)$/i', $offer_id, $matches ) ) + { + $country = (string) ( $matches[1] ?? '' ); + $rest = (string) ( $matches[2] ?? '' ); + + if ( $rest !== '' ) + { + $variants[] = 'shopify_' . strtolower( $country ) . '_' . $rest; + $variants[] = 'shopify_' . strtoupper( $country ) . '_' . $rest; + } + } + + return array_values( array_unique( array_filter( $variants ) ) ); + } + static private function get_shopping_ad_group_offer_ids_from_history( $client_id, $shopping_campaign_db_ids ) { global $mdb; @@ -2787,7 +5272,7 @@ class Cron return array_values( $scopes ); } - static private function sync_campaign_ad_groups_for_client( $campaigns_db_map, $customer_id, $api, $date_sync ) + static private function sync_campaign_ad_groups_for_client( $campaigns_db_map, $customer_id, $api, $date_sync, $as_of_date = null ) { global $mdb; @@ -2797,14 +5282,15 @@ class Cron return [ 'count' => 0, 'ad_group_map' => [], 'errors' => [] ]; } - $ad_groups_30 = $api -> get_ad_groups_30_days( $customer_id ); + $as_of_date = $as_of_date ? date( 'Y-m-d', strtotime( $as_of_date ) ) : date( 'Y-m-d', strtotime( $date_sync ) ); + $ad_groups_30 = $api -> get_ad_groups_30_days( $customer_id, $as_of_date ); if ( $ad_groups_30 === false ) { $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ); return [ 'count' => 0, 'ad_group_map' => [], 'errors' => [ 'Blad pobierania grup reklam (30 dni): ' . $last_err ] ]; } - $ad_groups_all_time = $api -> get_ad_groups_all_time( $customer_id ); + $ad_groups_all_time = $api -> get_ad_groups_all_time( $customer_id, $as_of_date ); if ( $ad_groups_all_time === false ) { $last_err = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ); @@ -2914,14 +5400,21 @@ class Cron } } - // Usun ad_groups ktore nie pojawiaja sie juz w API (zachowaj PMax placeholder ad_group_id=0) - if ( !empty( $seen_db_ids ) && !empty( $campaign_db_ids ) ) + // Usun ad_groups ktore nie pojawiaja sie juz w API (zachowaj PMax placeholder ad_group_id=0). + // Gdy API zwroci 0 aktywnych grup, usuwamy wszystkie historyczne grupy dla kampanii. + if ( !empty( $campaign_db_ids ) ) { - $mdb -> delete( 'campaign_ad_groups', [ 'AND' => [ + $delete_where = [ 'campaign_id' => $campaign_db_ids, - 'id[!]' => $seen_db_ids, 'ad_group_id[!]' => 0 - ] ] ); + ]; + + if ( !empty( $seen_db_ids ) ) + { + $delete_where['id[!]'] = $seen_db_ids; + } + + $mdb -> delete( 'campaign_ad_groups', $delete_where ); } return [ 'count' => $count, 'ad_group_map' => $ad_group_db_map, 'errors' => [] ]; @@ -3705,11 +6198,12 @@ class Cron static private function get_active_client( $pipeline ) { global $mdb; + $clients_not_deleted_sql_c = self::sql_clients_not_deleted( 'c' ); $row = $mdb -> query( "SELECT DISTINCT cs.client_id FROM cron_sync_status cs - INNER JOIN clients c ON cs.client_id = c.id AND c.deleted = 0 + INNER JOIN clients c ON cs.client_id = c.id AND " . $clients_not_deleted_sql_c . " WHERE cs.pipeline = :pipeline AND cs.phase != 'done' AND cs.sync_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY) @@ -3770,6 +6264,7 @@ class Cron static private function cleanup_old_sync_rows( $days = 30 ) { global $mdb; + $clients_deleted_sql_c = self::sql_clients_deleted( 'c' ); $mdb -> query( "DELETE FROM cron_sync_status WHERE phase = 'done' AND completed_at < DATE_SUB(NOW(), INTERVAL :days DAY)", @@ -3777,27 +6272,80 @@ class Cron ); $mdb -> query( - "DELETE cs FROM cron_sync_status cs LEFT JOIN clients c ON cs.client_id = c.id WHERE c.id IS NULL OR c.deleted = 1" + "DELETE cs + FROM cron_sync_status cs + LEFT JOIN clients c ON cs.client_id = c.id + WHERE c.id IS NULL OR " . $clients_deleted_sql_c ); } - static private function get_conversion_window_days() + static private function get_conversion_window_days( $prefer_config = false ) { + global $settings; + $request_value = (int) \S::get( 'conversion_window_days' ); if ( $request_value > 0 ) { return min( 90, $request_value ); } + if ( $prefer_config ) + { + $config_value = (int) ( $settings['google_ads_conversion_window_days'] ?? 0 ); + if ( $config_value > 0 ) + { + return min( 90, $config_value ); + } + } + $setting_value = (int) self::get_setting_value( 'google_ads_conversion_window_days', 7 ); if ( $setting_value <= 0 ) { - return 7; + $config_value = (int) ( $settings['google_ads_conversion_window_days'] ?? 7 ); + if ( $config_value <= 0 ) + { + $config_value = 7; + } + return min( 90, $config_value ); } return min( 90, $setting_value ); } + static private function cleanup_pipeline_rows_outside_window( $pipeline, $sync_dates ) + { + global $mdb; + + $pipeline = trim( (string) $pipeline ); + if ( $pipeline === '' ) + { + return; + } + + $sync_dates = array_values( array_filter( array_map( function( $item ) + { + $date = date( 'Y-m-d', strtotime( (string) $item ) ); + return $date ?: null; + }, (array) $sync_dates ) ) ); + + if ( empty( $sync_dates ) ) + { + return; + } + + $quoted_dates = array_map( function( $d ) use ( $mdb ) + { + return $mdb -> pdo -> quote( $d ); + }, $sync_dates ); + + $mdb -> query( + "DELETE FROM cron_sync_status + WHERE pipeline = :pipeline + AND sync_date NOT IN (" . implode( ',', $quoted_dates ) . ")", + [ ':pipeline' => $pipeline ] + ); + } + static private function build_backfill_dates( $end_date, $window_days ) { $end_timestamp = strtotime( $end_date ); @@ -3817,6 +6365,123 @@ class Cron return $dates; } + static private function is_debug_requested() + { + $raw = \S::get( 'debug' ); + + if ( is_bool( $raw ) ) + { + return $raw; + } + + $value = strtolower( trim( (string) $raw ) ); + return in_array( $value, [ '1', 'true', 'yes', 'on' ], true ); + } + + static private function output_cron_response( $payload ) + { + $payload = is_array( $payload ) ? $payload : [ 'result' => (string) $payload ]; + + if ( self::is_debug_requested() ) + { + header( 'Content-Type: text/html; charset=utf-8' ); + $pretty = json_encode( $payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT ); + if ( $pretty === false ) + { + $pretty = '{}'; + } + + $result_text = htmlspecialchars( (string) ( $payload['result'] ?? 'Brak komunikatu' ), ENT_QUOTES, 'UTF-8' ); + $active_client_id = isset( $payload['active_client_id'] ) ? (string) $payload['active_client_id'] : ( isset( $payload['client_id'] ) ? (string) $payload['client_id'] : '-' ); + $active_date = htmlspecialchars( (string) ( $payload['active_date'] ?? ( $payload['date'] ?? '-' ) ), ENT_QUOTES, 'UTF-8' ); + $errors_count = is_array( $payload['errors'] ?? null ) ? count( $payload['errors'] ) : 0; + + echo 'CRON debug'; + echo ''; + echo '

CRON debug

'; + echo '
Wynik
' . $result_text . '
'; + echo '
Klient ID
' . htmlspecialchars( $active_client_id, ENT_QUOTES, 'UTF-8' ) . '
'; + echo '
Data aktywna
' . $active_date . '
'; + echo '
Bledy
' . $errors_count . '
'; + echo '
'; + echo '
' . htmlspecialchars( $pretty, ENT_QUOTES, 'UTF-8' ) . '
'; + echo ''; + exit; + } + + header( 'Content-Type: application/json; charset=utf-8' ); + echo json_encode( $payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); + exit; + } + + static private function clients_has_column( $column_name ) + { + global $mdb; + + static $cache = []; + $column_name = trim( (string) $column_name ); + if ( $column_name === '' ) + { + return false; + } + + if ( isset( $cache[ $column_name ] ) ) + { + return (bool) $cache[ $column_name ]; + } + + $exists = false; + try + { + $stmt = $mdb -> pdo -> prepare( 'SHOW COLUMNS FROM clients LIKE :column_name' ); + if ( $stmt ) + { + $stmt -> bindValue( ':column_name', $column_name, \PDO::PARAM_STR ); + if ( $stmt -> execute() ) + { + $exists = (bool) $stmt -> fetch( \PDO::FETCH_ASSOC ); + } + } + } + catch ( \Throwable $e ) + { + $exists = false; + } + + $cache[ $column_name ] = $exists ? 1 : 0; + return $exists; + } + + static private function sql_clients_not_deleted( $alias = '' ) + { + $alias = trim( (string) $alias ); + $prefix = $alias !== '' ? $alias . '.' : ''; + + if ( self::clients_has_column( 'deleted' ) ) + { + return 'COALESCE(' . $prefix . 'deleted, 0) = 0'; + } + + return '1=1'; + } + + static private function sql_clients_deleted( $alias = '' ) + { + $alias = trim( (string) $alias ); + $prefix = $alias !== '' ? $alias . '.' : ''; + + if ( self::clients_has_column( 'deleted' ) ) + { + return 'COALESCE(' . $prefix . 'deleted, 0) = 1'; + } + + return '0=1'; + } + static private function get_setting_value( $setting_key, $default = null ) { global $mdb; diff --git a/autoload/controls/class.Products.php b/autoload/controls/class.Products.php index 1de176f..ba9ace8 100644 --- a/autoload/controls/class.Products.php +++ b/autoload/controls/class.Products.php @@ -201,7 +201,15 @@ class Products static public function get_campaigns_list() { $client_id = (int) \S::get( 'client_id' ); - echo json_encode( [ 'campaigns' => \factory\Campaigns::get_campaigns_list( $client_id, true ) ] ); + $campaigns = \factory\Campaigns::get_campaigns_list( $client_id, true ); + $allowed_channel_types = [ 'SHOPPING', 'PERFORMANCE_MAX' ]; + + $campaigns = array_values( array_filter( (array) $campaigns, function( $row ) use ( $allowed_channel_types ) { + $channel_type = strtoupper( trim( (string) ( $row['advertising_channel_type'] ?? '' ) ) ); + return in_array( $channel_type, $allowed_channel_types, true ); + } ) ); + + echo json_encode( [ 'campaigns' => $campaigns ] ); exit; } @@ -219,6 +227,169 @@ class Products exit; } + static public function get_scope_alerts() + { + $client_id = (int) \S::get( 'client_id' ); + $campaign_id = (int) \S::get( 'campaign_id' ); + $ad_group_id = (int) \S::get( 'ad_group_id' ); + + $alerts = \factory\Products::get_scope_alerts( $client_id, $campaign_id, $ad_group_id, 80 ); + + echo json_encode( [ + 'status' => 'ok', + 'alerts' => $alerts, + 'count' => count( $alerts ) + ] ); + exit; + } + + static public function get_products_without_impressions_30() + { + $client_id = (int) \S::get( 'client_id' ); + $campaign_id = (int) \S::get( 'campaign_id' ); + $ad_group_id = (int) \S::get( 'ad_group_id' ); + + if ( $client_id <= 0 || $campaign_id <= 0 ) + { + echo json_encode( [ + 'status' => 'ok', + 'products' => [], + 'count' => 0 + ] ); + exit; + } + + $rows = \factory\Products::get_products_without_impressions_30( $client_id, $campaign_id, $ad_group_id, 1000 ); + $products = []; + + foreach ( (array) $rows as $row ) + { + $product_id = (int) ( $row['product_id'] ?? 0 ); + if ( $product_id <= 0 ) + { + continue; + } + + $offer_id = trim( (string) ( $row['offer_id'] ?? '' ) ); + $product_name = trim( (string) ( $row['title'] ?? '' ) ); + if ( $product_name === '' ) + { + $product_name = trim( (string) ( $row['name'] ?? '' ) ); + } + if ( $product_name === '' ) + { + $product_name = $offer_id !== '' ? $offer_id : ( 'Produkt #' . $product_id ); + } + + $products[] = [ + 'product_id' => $product_id, + 'offer_id' => $offer_id, + 'name' => $product_name + ]; + } + + echo json_encode( [ + 'status' => 'ok', + 'products' => $products, + 'count' => count( $products ) + ] ); + exit; + } + + static public function delete_campaign_ad_group() + { + $campaign_id = (int) \S::get( 'campaign_id' ); + $ad_group_id = (int) \S::get( 'ad_group_id' ); + $delete_scope = trim( (string) \S::get( 'delete_scope' ) ); + + if ( $ad_group_id <= 0 ) + { + echo json_encode( [ 'status' => 'error', 'message' => 'Nie wybrano grupy reklam do usuniecia.' ] ); + exit; + } + + if ( !in_array( $delete_scope, [ 'local', 'google' ], true ) ) + { + $delete_scope = 'local'; + } + + $context = \factory\Products::get_ad_group_delete_context( $ad_group_id ); + if ( !$context ) + { + echo json_encode( [ 'status' => 'ok', 'message' => 'Grupa reklam byla juz usunieta lokalnie.' ] ); + exit; + } + + $local_campaign_id = (int) ( $context['local_campaign_id'] ?? 0 ); + if ( $campaign_id > 0 && $campaign_id !== $local_campaign_id ) + { + echo json_encode( [ 'status' => 'error', 'message' => 'Wybrana grupa reklam nie nalezy do wskazanej kampanii.' ] ); + exit; + } + + $channel_type = strtoupper( trim( (string) ( $context['advertising_channel_type'] ?? '' ) ) ); + if ( $channel_type !== 'SHOPPING' ) + { + echo json_encode( [ 'status' => 'error', 'message' => 'Usuwanie grup reklam jest dostepne tylko dla kampanii produktowych (Shopping).' ] ); + exit; + } + + if ( $delete_scope === 'google' ) + { + $customer_id = preg_replace( '/\D+/', '', (string) ( $context['google_ads_customer_id'] ?? '' ) ); + $external_ad_group_id = (int) ( $context['external_ad_group_id'] ?? 0 ); + + if ( $customer_id === '' || $external_ad_group_id <= 0 ) + { + echo json_encode( [ 'status' => 'error', 'message' => 'Brak danych Google Ads (customer_id lub ad_group_id).' ] ); + exit; + } + + $google_ads_api = new \services\GoogleAdsApi(); + if ( !$google_ads_api -> is_configured() ) + { + echo json_encode( [ 'status' => 'error', 'message' => 'Google Ads API nie jest skonfigurowane.' ] ); + exit; + } + + $google_result = $google_ads_api -> remove_ad_group( $customer_id, $external_ad_group_id ); + if ( empty( $google_result['success'] ) ) + { + $error_message = trim( (string) ( $google_result['error'] ?? \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ) ) ); + if ( $error_message === '' ) + { + $error_message = 'Nie udalo sie usunac grupy reklam w Google Ads.'; + } + + echo json_encode( [ 'status' => 'error', 'message' => $error_message ] ); + exit; + } + } + + if ( !\factory\Products::delete_ad_group_local( $ad_group_id ) ) + { + $still_exists_local = \factory\Products::get_ad_group_scope_context( $ad_group_id ); + if ( !$still_exists_local ) + { + echo json_encode( [ 'status' => 'ok', 'message' => 'Grupa reklam zostala usunieta lokalnie.' ] ); + exit; + } + + echo json_encode( [ 'status' => 'error', 'message' => 'Nie udalo sie usunac grupy reklam lokalnie.' ] ); + exit; + } + + if ( $delete_scope === 'google' ) + { + echo json_encode( [ 'status' => 'ok', 'message' => 'Usunieto grupe reklam lokalnie oraz w Google Ads.' ] ); + } + else + { + echo json_encode( [ 'status' => 'ok', 'message' => 'Usunieto grupe reklam lokalnie.' ] ); + } + exit; + } + static public function assign_product_scope() { $product_id = (int) \S::get( 'product_id' ); diff --git a/autoload/controls/class.Users.php b/autoload/controls/class.Users.php index 2cde5e5..68cae9f 100644 --- a/autoload/controls/class.Users.php +++ b/autoload/controls/class.Users.php @@ -154,7 +154,19 @@ class Users global $mdb; $base_url = self::get_base_url(); - $clients_total = (int) $mdb -> query( "SELECT COUNT(*) FROM clients WHERE deleted = 0 AND google_ads_customer_id IS NOT NULL AND google_ads_customer_id <> ''" ) -> fetchColumn(); + $clients_total = (int) $mdb -> query( + "SELECT COUNT(*) + FROM clients + WHERE COALESCE( active, 0 ) = 1 + AND TRIM( COALESCE( google_ads_customer_id, '' ) ) <> ''" + ) -> fetchColumn(); + $merchant_clients_total = (int) $mdb -> query( + "SELECT COUNT(*) + FROM clients + WHERE COALESCE( active, 0 ) = 1 + AND TRIM( COALESCE( google_ads_customer_id, '' ) ) <> '' + AND TRIM( COALESCE( google_merchant_account_id, '' ) ) <> ''" + ) -> fetchColumn(); // --- Kampanie --- $campaign_stats = $mdb -> query( @@ -203,7 +215,7 @@ class Users if ( $products_active_date ) { $current_phase = $mdb -> query( - "SELECT phase FROM cron_sync_status cs INNER JOIN clients c ON cs.client_id = c.id AND c.deleted = 0 WHERE cs.pipeline = 'products' AND cs.sync_date = :sync_date AND cs.phase != 'done' ORDER BY FIELD(cs.phase, 'pending', 'fetch', 'aggregate_30') ASC LIMIT 1", + "SELECT phase FROM cron_sync_status cs INNER JOIN clients c ON cs.client_id = c.id AND COALESCE(c.active, 0) = 1 WHERE cs.pipeline = 'products' AND cs.sync_date = :sync_date AND cs.phase != 'done' ORDER BY FIELD(cs.phase, 'pending', 'fetch', 'aggregate_30') ASC LIMIT 1", [ ':sync_date' => $products_active_date ] ) -> fetchColumn(); @@ -222,16 +234,45 @@ class Users $products_meta .= ', ' . $products_eta_meta; } + // --- Walidacja Merchant dla alertow kampanii --- + $merchant_cursor_client_id = (int) \services\GoogleAdsApi::get_setting( 'cron_campaigns_product_alerts_last_client_id' ); + $merchant_processed = 0; + if ( $merchant_cursor_client_id > 0 && $merchant_clients_total > 0 ) + { + $merchant_processed = (int) $mdb -> query( + "SELECT COUNT(*) + FROM clients + WHERE COALESCE( active, 0 ) = 1 + AND TRIM( COALESCE( google_ads_customer_id, '' ) ) <> '' + AND TRIM( COALESCE( google_merchant_account_id, '' ) ) <> '' + AND id <= :cursor_client_id", + [ ':cursor_client_id' => $merchant_cursor_client_id ] + ) -> fetchColumn(); + } + + $merchant_processed = min( $merchant_clients_total, max( 0, $merchant_processed ) ); + $merchant_remaining = max( 0, $merchant_clients_total - $merchant_processed ); + $merchant_meta = 'Kursor klienta: ' . ( $merchant_cursor_client_id > 0 ? '#' . $merchant_cursor_client_id : '-' ) . ', klienci z Merchant ID: ' . $merchant_clients_total; + $merchant_eta_meta = self::build_eta_meta( 'cron_campaigns_product_alerts_merchant', $merchant_remaining ); + if ( $merchant_eta_meta !== '' ) + { + $merchant_meta .= ', ' . $merchant_eta_meta; + } + + $cron_schedule = []; + // --- Endpointy CRON --- $cron_endpoints = [ - [ 'name' => 'Legacy CRON', 'path' => '/cron.php', 'action' => 'cron_legacy' ], - [ 'name' => 'Cron kampanii', 'path' => '/cron/cron_campaigns', 'action' => 'cron_campaigns' ], - [ 'name' => 'Cron produktów', 'path' => '/cron/cron_products', 'action' => 'cron_products' ], - [ 'name' => 'Cron URL produktów (Merchant)', 'path' => '/cron/cron_products_urls', 'action' => 'cron_products_urls' ], - [ 'name' => 'Cron fraz', 'path' => '/cron/cron_phrases', 'action' => 'cron_phrases' ], - [ 'name' => 'Historia 30 dni produktów', 'path' => '/cron/cron_products_history_30', 'action' => 'cron_products_history_30' ], - [ 'name' => 'Historia 30 dni fraz', 'path' => '/cron/cron_phrases_history_30', 'action' => 'cron_phrases_history_30' ], - [ 'name' => 'Eksport XML', 'path' => '/cron/cron_xml', 'action' => 'cron_xml' ], + [ 'name' => 'Legacy CRON', 'path' => '/cron.php', 'action' => 'cron_legacy', 'plan' => '' ], + [ 'name' => 'Cron zbiorczy (K->P->GMC)', 'path' => '/cron/cron_clients_bundle', 'action' => 'cron_clients_bundle', 'plan' => 'Co 1 min: klient -> kampanie (-7..-1) -> produkty (-7..-1) -> GMC' ], + [ 'name' => 'Cron kampanii', 'path' => '/cron/cron_campaigns', 'action' => 'cron_campaigns', 'plan' => 'Krok 1/2, co 15 min' ], + [ 'name' => 'Cron alertow kampanii (Merchant)', 'path' => '/cron/cron_campaigns_product_alerts_merchant', 'action' => 'cron_campaigns_product_alerts_merchant', 'plan' => 'Krok 2/2, 2-5 min po Cron kampanii, co 15 min' ], + [ 'name' => 'Cron produktów', 'path' => '/cron/cron_products', 'action' => 'cron_products', 'plan' => '' ], + [ 'name' => 'Cron URL produktów (Merchant)', 'path' => '/cron/cron_products_urls', 'action' => 'cron_products_urls', 'plan' => '' ], + [ 'name' => 'Cron fraz', 'path' => '/cron/cron_phrases', 'action' => 'cron_phrases', 'plan' => '' ], + [ 'name' => 'Historia 30 dni produktów', 'path' => '/cron/cron_products_history_30', 'action' => 'cron_products_history_30', 'plan' => '' ], + [ 'name' => 'Historia 30 dni fraz', 'path' => '/cron/cron_phrases_history_30', 'action' => 'cron_phrases_history_30', 'plan' => '' ], + [ 'name' => 'Eksport XML', 'path' => '/cron/cron_xml', 'action' => 'cron_xml', 'plan' => '' ], ]; $urls = []; @@ -242,6 +283,7 @@ class Users 'name' => $endpoint['name'], 'url' => $base_url . $endpoint['path'], 'last_invoked_at' => self::format_datetime( \services\GoogleAdsApi::get_setting( $last_key ) ), + 'plan' => (string) ( $endpoint['plan'] ?? '' ), ]; } @@ -263,7 +305,15 @@ class Users 'percent' => self::progress_percent( $products_processed, $products_total ), 'meta' => $products_meta ], + [ + 'name' => 'Walidacja Merchant', + 'processed' => $merchant_processed, + 'total' => $merchant_clients_total, + 'percent' => self::progress_percent( $merchant_processed, $merchant_clients_total ), + 'meta' => $merchant_meta + ], ], + 'schedule' => $cron_schedule, 'urls' => $urls ]; } diff --git a/autoload/factory/class.CampaignAlerts.php b/autoload/factory/class.CampaignAlerts.php index cf2ebcc..e6e7080 100644 --- a/autoload/factory/class.CampaignAlerts.php +++ b/autoload/factory/class.CampaignAlerts.php @@ -57,6 +57,7 @@ class CampaignAlerts ca.campaign_external_id, ca.ad_group_id, ca.ad_group_external_id, + ca.product_id, ca.alert_type, ca.message, ca.meta_json, @@ -109,4 +110,29 @@ class CampaignAlerts 'date_detected[<]' => date( 'Y-m-d', strtotime( '-' . (int) $days . ' days' ) ) ] ); } + + static public function delete_alerts( array $ids ) + { + global $mdb; + + $ids = array_map( 'intval', $ids ); + $ids = array_filter( $ids, function( $id ) { return $id > 0; } ); + + if ( empty( $ids ) ) + { + return 0; + } + + $before = (int) $mdb -> count( 'campaign_alerts', [ 'id' => $ids ] ); + if ( $before <= 0 ) + { + return 0; + } + + $mdb -> delete( 'campaign_alerts', [ 'id' => $ids ] ); + + $after = (int) $mdb -> count( 'campaign_alerts', [ 'id' => $ids ] ); + + return max( 0, $before - $after ); + } } diff --git a/autoload/factory/class.Products.php b/autoload/factory/class.Products.php index 4bb63f4..439f8b5 100644 --- a/autoload/factory/class.Products.php +++ b/autoload/factory/class.Products.php @@ -2,6 +2,18 @@ namespace factory; class Products { + static private function is_product_core_field( $field ) + { + return in_array( (string) $field, [ + 'custom_label_4', + 'custom_label_3', + 'title', + 'description', + 'google_product_category', + 'product_url' + ], true ); + } + static public function delete_product( $product_id ) { global $mdb; $mdb -> delete( 'products', [ 'id' => $product_id ] ); @@ -31,12 +43,162 @@ class Products return $mdb -> delete( 'products_comments', [ 'id' => $comment_id ] ); } + static public function get_ad_group_delete_context( $ad_group_id ) + { + global $mdb; + + return $mdb -> query( + 'SELECT + ag.id AS local_ad_group_id, + ag.campaign_id AS local_campaign_id, + ag.ad_group_id AS external_ad_group_id, + ag.ad_group_name, + c.client_id, + c.campaign_id AS external_campaign_id, + c.campaign_name, + c.advertising_channel_type, + cl.google_ads_customer_id + FROM campaign_ad_groups AS ag + INNER JOIN campaigns AS c ON c.id = ag.campaign_id + INNER JOIN clients AS cl ON cl.id = c.client_id + WHERE ag.id = :ad_group_id + LIMIT 1', + [ ':ad_group_id' => (int) $ad_group_id ] + ) -> fetch( \PDO::FETCH_ASSOC ); + } + + static public function delete_ad_group_local( $ad_group_id ) + { + global $mdb; + + $ad_group_id = (int) $ad_group_id; + if ( $ad_group_id <= 0 ) + { + return false; + } + + $mdb -> delete( 'campaign_ad_groups', [ 'id' => $ad_group_id ] ); + + if ( (int) $mdb -> rowCount() > 0 ) + { + return true; + } + + // Traktuj jako sukces, jeżeli wpis i tak już nie istnieje. + $exists = (int) $mdb -> count( 'campaign_ad_groups', [ 'id' => $ad_group_id ] ); + return $exists === 0; + } + static public function get_product_comment_by_date( $product_id, $date ) { global $mdb; return $mdb -> get( 'products_comments', [ 'id', 'comment' ], [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] ); } + static public function get_scope_alerts( $client_id, $campaign_id, $ad_group_id, $limit = 50 ) + { + global $mdb; + + $client_id = (int) $client_id; + $campaign_id = (int) $campaign_id; + $ad_group_id = (int) $ad_group_id; + $limit = max( 1, (int) $limit ); + + if ( $client_id <= 0 || $campaign_id <= 0 ) + { + return []; + } + + $where = [ + 'client_id' => $client_id, + 'campaign_id' => $campaign_id + ]; + + if ( $ad_group_id > 0 ) + { + $where['ad_group_id'] = $ad_group_id; + } + + $rows = $mdb -> select( 'campaign_alerts', [ + 'id', + 'alert_type', + 'message', + 'date_detected', + 'date_add' + ], [ + 'AND' => $where, + 'ORDER' => [ + 'date_detected' => 'DESC', + 'id' => 'DESC' + ], + 'LIMIT' => $limit + ] ); + + return is_array( $rows ) ? $rows : []; + } + + static public function get_products_without_impressions_30( $client_id, $campaign_id, $ad_group_id = 0, $limit = 500 ) + { + global $mdb; + + $client_id = (int) $client_id; + $campaign_id = (int) $campaign_id; + $ad_group_id = (int) $ad_group_id; + $limit = max( 1, (int) $limit ); + $limit = min( 2000, $limit ); + + if ( $client_id <= 0 || $campaign_id <= 0 ) + { + return []; + } + + $params = [ + ':client_id' => $client_id, + ':campaign_id' => $campaign_id + ]; + + $sql = 'SELECT + p.id AS product_id, + p.offer_id, + p.name, + p.title, + SUM( pa.impressions_30 ) AS impressions_30 + FROM products_aggregate AS pa + INNER JOIN products AS p ON p.id = pa.product_id + WHERE p.client_id = :client_id + AND pa.campaign_id = :campaign_id'; + + if ( $ad_group_id > 0 ) + { + $sql .= ' AND pa.ad_group_id = :ad_group_id'; + $params[':ad_group_id'] = $ad_group_id; + } + + $sql .= ' + GROUP BY p.id, p.offer_id, p.name, p.title + HAVING COALESCE( SUM( pa.impressions_30 ), 0 ) = 0 + ORDER BY COALESCE( NULLIF( TRIM( p.title ), \'\' ), NULLIF( TRIM( p.name ), \'\' ), p.offer_id ) ASC, p.id ASC + LIMIT ' . $limit; + + try + { + $statement = $mdb -> query( $sql, $params ); + if ( !$statement ) + { + error_log( '[products] get_products_without_impressions_30 query returned no statement.' ); + return []; + } + + $rows = $statement -> fetchAll( \PDO::FETCH_ASSOC ); + return is_array( $rows ) ? $rows : []; + } + catch ( \Throwable $e ) + { + error_log( '[products] get_products_without_impressions_30 query error: ' . $e -> getMessage() ); + return []; + } + } + static public function get_min_roas( $product_id ) { global $mdb; @@ -68,13 +230,13 @@ class Products if ( $campaign_id > 0 ) { - $sql .= ' AND pt.campaign_id = :campaign_id'; + $sql .= ' AND pa.campaign_id = :campaign_id'; $params[':campaign_id'] = $campaign_id; } if ( $ad_group_id > 0 ) { - $sql .= ' AND pt.ad_group_id = :ad_group_id'; + $sql .= ' AND pa.ad_group_id = :ad_group_id'; $params[':ad_group_id'] = $ad_group_id; } } @@ -112,44 +274,44 @@ class Products p.id AS product_id, p.offer_id, p.min_roas, - pt.campaign_id, + pa.campaign_id, CASE - WHEN COUNT( DISTINCT pt.campaign_id ) > 1 THEN \'--- wiele kampanii ---\' + WHEN COUNT( DISTINCT pa.campaign_id ) > 1 THEN \'--- wiele kampanii ---\' ELSE COALESCE( MAX( c.campaign_name ), \'--- brak kampanii ---\' ) END AS campaign_name, CASE - WHEN COUNT( DISTINCT pt.ad_group_id ) > 1 THEN \'--- wiele grup reklam ---\' - WHEN MAX( pt.ad_group_id ) = 0 THEN \'PMax (bez grup reklam)\' + WHEN COUNT( DISTINCT pa.ad_group_id ) > 1 THEN \'--- wiele grup reklam ---\' + WHEN MAX( pa.ad_group_id ) = 0 THEN \'PMax (bez grup reklam)\' ELSE COALESCE( MAX( ag.ad_group_name ), \'--- brak grupy reklam ---\' ) END AS ad_group_name, CASE - WHEN COUNT( DISTINCT pt.ad_group_id ) = 1 THEN MAX( pt.ad_group_id ) + WHEN COUNT( DISTINCT pa.ad_group_id ) = 1 THEN MAX( pa.ad_group_id ) ELSE 0 END AS ad_group_id, - MAX( pt.name ) AS name, - SUM( pt.impressions ) AS impressions, - SUM( pt.impressions_30 ) AS impressions_30, - SUM( pt.clicks ) AS clicks, - SUM( pt.clicks_30 ) AS clicks_30, + COALESCE( NULLIF( TRIM( p.title ), \'\' ), NULLIF( TRIM( p.name ), \'\' ), p.offer_id ) AS name, + SUM( pa.impressions_all_time ) AS impressions, + SUM( pa.impressions_30 ) AS impressions_30, + SUM( pa.clicks_all_time ) AS clicks, + SUM( pa.clicks_30 ) AS clicks_30, CASE - WHEN SUM( pt.impressions ) > 0 THEN ROUND( SUM( pt.clicks ) / SUM( pt.impressions ) * 100, 2 ) + WHEN SUM( pa.impressions_all_time ) > 0 THEN ROUND( SUM( pa.clicks_all_time ) / SUM( pa.impressions_all_time ) * 100, 2 ) ELSE 0 END AS ctr, - SUM( pt.cost ) AS cost, + SUM( pa.cost_all_time ) AS cost, CASE - WHEN SUM( pt.clicks ) > 0 THEN ROUND( SUM( pt.cost ) / SUM( pt.clicks ), 6 ) + WHEN SUM( pa.clicks_all_time ) > 0 THEN ROUND( SUM( pa.cost_all_time ) / SUM( pa.clicks_all_time ), 6 ) ELSE 0 END AS cpc, - SUM( pt.conversions ) AS conversions, - SUM( pt.conversions_value ) AS conversions_value, + SUM( pa.conversions_all_time ) AS conversions, + SUM( pa.conversion_value_all_time ) AS conversions_value, CASE - WHEN SUM( pt.cost ) > 0 THEN ROUND( SUM( pt.conversions_value ) / SUM( pt.cost ) * 100, 2 ) + WHEN SUM( pa.cost_all_time ) > 0 THEN ROUND( SUM( pa.conversion_value_all_time ) / SUM( pa.cost_all_time ) * 100, 2 ) ELSE 0 END AS roas - FROM products_temp AS pt - INNER JOIN products AS p ON p.id = pt.product_id - LEFT JOIN campaigns AS c ON c.id = pt.campaign_id - LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id + FROM products_aggregate AS pa + INNER JOIN products AS p ON p.id = pa.product_id + LEFT JOIN campaigns AS c ON c.id = pa.campaign_id + LEFT JOIN campaign_ad_groups AS ag ON ag.id = pa.ad_group_id WHERE p.client_id = :client_id'; self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id ); @@ -157,7 +319,8 @@ class Products if ( $search ) { $sql .= ' AND ( - pt.name LIKE :search + p.name LIKE :search + OR p.title LIKE :search OR p.offer_id LIKE :search OR c.campaign_name LIKE :search OR ag.ad_group_name LIKE :search @@ -165,7 +328,7 @@ class Products $params[':search'] = '%' . $search . '%'; } - $sql .= ' GROUP BY p.id, p.offer_id, p.min_roas, pt.campaign_id'; + $sql .= ' GROUP BY p.id, p.offer_id, p.min_roas, pa.campaign_id, p.name, p.title'; $sql .= ' ORDER BY ' . $order_sql . ' ' . $order_dir . ', product_id DESC LIMIT ' . $start . ', ' . $limit; return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC ); @@ -177,20 +340,27 @@ class Products $params = [ ':client_id' => $client_id ]; - $sql = 'SELECT MIN( p.min_roas ) AS min_roas, MAX( pt.roas ) AS max_roas - FROM products_temp AS pt - INNER JOIN products AS p ON p.id = pt.product_id - LEFT JOIN campaigns AS c ON c.id = pt.campaign_id - LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id + $sql = 'SELECT MIN( p.min_roas ) AS min_roas, + MAX( + CASE + WHEN COALESCE( pa.cost_all_time, 0 ) > 0 THEN ROUND( COALESCE( pa.conversion_value_all_time, 0 ) / pa.cost_all_time * 100, 2 ) + ELSE 0 + END + ) AS max_roas + FROM products_aggregate AS pa + INNER JOIN products AS p ON p.id = pa.product_id + LEFT JOIN campaigns AS c ON c.id = pa.campaign_id + LEFT JOIN campaign_ad_groups AS ag ON ag.id = pa.ad_group_id WHERE p.client_id = :client_id - AND pt.conversions > 10'; + AND pa.conversions_all_time > 10'; self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id ); if ( $search ) { $sql .= ' AND ( - pt.name LIKE :search + p.name LIKE :search + OR p.title LIKE :search OR p.offer_id LIKE :search OR c.campaign_name LIKE :search OR ag.ad_group_name LIKE :search @@ -213,11 +383,11 @@ class Products $params = [ ':client_id' => (int) $client_id ]; $sql = 'SELECT COUNT(0) FROM ( - SELECT p.id, pt.campaign_id - FROM products_temp AS pt - INNER JOIN products AS p ON p.id = pt.product_id - LEFT JOIN campaigns AS c ON c.id = pt.campaign_id - LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id + SELECT p.id, pa.campaign_id + FROM products_aggregate AS pa + INNER JOIN products AS p ON p.id = pa.product_id + LEFT JOIN campaigns AS c ON c.id = pa.campaign_id + LEFT JOIN campaign_ad_groups AS ag ON ag.id = pa.ad_group_id WHERE p.client_id = :client_id'; self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id ); @@ -225,7 +395,8 @@ class Products if ( $search ) { $sql .= ' AND ( - pt.name LIKE :search + p.name LIKE :search + OR p.title LIKE :search OR p.offer_id LIKE :search OR c.campaign_name LIKE :search OR ag.ad_group_name LIKE :search @@ -233,7 +404,7 @@ class Products $params[':search'] = '%' . $search . '%'; } - $sql .= ' GROUP BY p.id, pt.campaign_id + $sql .= ' GROUP BY p.id, pa.campaign_id ) AS grouped_rows'; return $mdb -> query( $sql, $params ) -> fetchColumn(); @@ -248,30 +419,30 @@ class Products p.offer_id, p.name, p.min_roas, - COALESCE( SUM( pt.impressions ), 0 ) AS impressions, - COALESCE( SUM( pt.impressions_30 ), 0 ) AS impressions_30, - COALESCE( SUM( pt.clicks ), 0 ) AS clicks, - COALESCE( SUM( pt.clicks_30 ), 0 ) AS clicks_30, + COALESCE( SUM( pa.impressions_all_time ), 0 ) AS impressions, + COALESCE( SUM( pa.impressions_30 ), 0 ) AS impressions_30, + COALESCE( SUM( pa.clicks_all_time ), 0 ) AS clicks, + COALESCE( SUM( pa.clicks_30 ), 0 ) AS clicks_30, CASE - WHEN COALESCE( SUM( pt.impressions ), 0 ) > 0 - THEN ROUND( COALESCE( SUM( pt.clicks ), 0 ) / COALESCE( SUM( pt.impressions ), 0 ) * 100, 2 ) + WHEN COALESCE( SUM( pa.impressions_all_time ), 0 ) > 0 + THEN ROUND( COALESCE( SUM( pa.clicks_all_time ), 0 ) / COALESCE( SUM( pa.impressions_all_time ), 0 ) * 100, 2 ) ELSE 0 END AS ctr, - COALESCE( SUM( pt.cost ), 0 ) AS cost, + COALESCE( SUM( pa.cost_all_time ), 0 ) AS cost, CASE - WHEN COALESCE( SUM( pt.clicks ), 0 ) > 0 - THEN ROUND( COALESCE( SUM( pt.cost ), 0 ) / COALESCE( SUM( pt.clicks ), 0 ), 6 ) + WHEN COALESCE( SUM( pa.clicks_all_time ), 0 ) > 0 + THEN ROUND( COALESCE( SUM( pa.cost_all_time ), 0 ) / COALESCE( SUM( pa.clicks_all_time ), 0 ), 6 ) ELSE 0 END AS cpc, - COALESCE( SUM( pt.conversions ), 0 ) AS conversions, - COALESCE( SUM( pt.conversions_value ), 0 ) AS conversions_value, + COALESCE( SUM( pa.conversions_all_time ), 0 ) AS conversions, + COALESCE( SUM( pa.conversion_value_all_time ), 0 ) AS conversions_value, CASE - WHEN COALESCE( SUM( pt.cost ), 0 ) > 0 - THEN ROUND( COALESCE( SUM( pt.conversions_value ), 0 ) / COALESCE( SUM( pt.cost ), 0 ) * 100, 2 ) + WHEN COALESCE( SUM( pa.cost_all_time ), 0 ) > 0 + THEN ROUND( COALESCE( SUM( pa.conversion_value_all_time ), 0 ) / COALESCE( SUM( pa.cost_all_time ), 0 ) * 100, 2 ) ELSE 0 END AS roas FROM products AS p - LEFT JOIN products_temp AS pt ON pt.product_id = p.id + LEFT JOIN products_aggregate AS pa ON pa.product_id = p.id WHERE p.id = :pid GROUP BY p.id, p.offer_id, p.name, p.min_roas', [ ':pid' => $product_id ] @@ -281,7 +452,21 @@ class Products static public function get_product_data( $product_id, $field ) { global $mdb; - return $mdb -> get( 'products_data', $field, [ 'product_id' => $product_id ] ); + + $product_id = (int) $product_id; + $field = trim( (string) $field ); + + if ( $product_id <= 0 || $field === '' ) + { + return null; + } + + if ( !self::is_product_core_field( $field ) ) + { + return null; + } + + return $mdb -> get( 'products', $field, [ 'id' => $product_id ] ); } static public function get_product_merchant_context( $product_id ) @@ -459,10 +644,36 @@ class Products { global $mdb; - if ( !$mdb -> count( 'products_data', [ 'product_id' => $product_id ] ) ) - $result = $mdb -> insert( 'products_data', [ 'product_id' => $product_id, $field => $value ] ); - else - $result = $mdb -> update( 'products_data', [ $field => $value ], [ 'product_id' => $product_id ] ); + $product_id = (int) $product_id; + $field = trim( (string) $field ); + + if ( $product_id <= 0 || $field === '' ) + { + return false; + } + + $result = false; + if ( self::is_product_core_field( $field ) ) + { + $update_data = [ $field => $value ]; + if ( $field === 'product_url' ) + { + $product_url = trim( (string) $value ); + if ( $product_url === '' || in_array( strtolower( $product_url ), [ '0', '-', 'null' ], true ) ) + { + $update_data['product_url'] = null; + } + else + { + $update_data['merchant_url_not_found'] = 0; + } + + $update_data['merchant_url_last_check'] = date( 'Y-m-d H:i:s' ); + } + + $result = $mdb -> update( 'products', $update_data, [ 'id' => $product_id ] ); + } + return $result; } @@ -810,7 +1021,7 @@ class Products return false; } - $scope_exists = (int) $mdb -> count( 'products_temp', [ + $scope_exists = (int) $mdb -> count( 'products_aggregate', [ 'AND' => [ 'product_id' => $product_id, 'campaign_id' => $campaign_id, @@ -820,34 +1031,28 @@ class Products if ( $scope_exists ) { - $mdb -> update( 'products_temp', [ - 'name' => $product['name'] - ], [ - 'AND' => [ - 'product_id' => $product_id, - 'campaign_id' => $campaign_id, - 'ad_group_id' => $ad_group_id - ] - ] ); - return true; } - return $mdb -> insert( 'products_temp', [ + return $mdb -> insert( 'products_aggregate', [ 'product_id' => $product_id, 'campaign_id' => $campaign_id, 'ad_group_id' => $ad_group_id, - 'name' => $product['name'], - 'impressions' => 0, 'impressions_30' => 0, - 'clicks' => 0, 'clicks_30' => 0, - 'ctr' => 0, - 'cost' => 0, - 'conversions' => 0, - 'conversions_value' => 0, - 'cpc' => 0, - 'roas' => 0 + 'ctr_30' => 0, + 'cost_30' => 0, + 'conversions_30' => 0, + 'conversion_value_30' => 0, + 'roas_30' => 0, + 'impressions_all_time' => 0, + 'clicks_all_time' => 0, + 'ctr_all_time' => 0, + 'cost_all_time' => 0, + 'conversions_all_time' => 0, + 'conversion_value_all_time' => 0, + 'roas_all_time' => 0, + 'date_sync' => date( 'Y-m-d' ) ] ); } } diff --git a/autoload/services/class.GoogleAdsApi.php b/autoload/services/class.GoogleAdsApi.php index 8200823..cacef7b 100644 --- a/autoload/services/class.GoogleAdsApi.php +++ b/autoload/services/class.GoogleAdsApi.php @@ -1023,6 +1023,58 @@ class GoogleAdsApi return $this -> mutate( $customer_id, [ $operation ] ) !== false; } + public function remove_ad_group( $customer_id, $ad_group_id ) + { + $customer_id = $this -> normalize_ads_customer_id( $customer_id ); + $ad_group_id = (int) $ad_group_id; + + if ( $customer_id === '' || $ad_group_id <= 0 ) + { + self::set_setting( 'google_ads_last_error', 'Brak danych do usuniecia grupy reklam.' ); + return [ 'success' => false, 'removed' => 0 ]; + } + + $resource_name = 'customers/' . $customer_id . '/adGroups/' . $ad_group_id; + $operation = [ + 'adGroupOperation' => [ + 'remove' => $resource_name + ] + ]; + + $mutate_result = $this -> mutate( $customer_id, [ $operation ] ); + + if ( $mutate_result === false ) + { + $last_error = (string) self::get_setting( 'google_ads_last_error' ); + $is_not_found = stripos( $last_error, 'NOT_FOUND' ) !== false + || stripos( $last_error, 'RESOURCE_NOT_FOUND' ) !== false; + + if ( $is_not_found ) + { + return [ + 'success' => true, + 'removed' => 0, + 'not_found' => true, + 'resource_name' => $resource_name + ]; + } + + return [ + 'success' => false, + 'removed' => 0, + 'error' => $last_error, + 'resource_name' => $resource_name + ]; + } + + return [ + 'success' => true, + 'removed' => 1, + 'response' => $mutate_result, + 'resource_name' => $resource_name + ]; + } + private function get_root_listing_group_resource_name( $customer_id, $ad_group_id ) { $customer_id = $this -> normalize_ads_customer_id( $customer_id ); @@ -2468,8 +2520,11 @@ class GoogleAdsApi return array_values( $campaigns ); } - public function get_ad_groups_30_days( $customer_id ) + public function get_ad_groups_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, " . "ad_group.id, " @@ -2482,7 +2537,8 @@ class GoogleAdsApi . "FROM ad_group " . "WHERE campaign.status = 'ENABLED' " . "AND ad_group.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; @@ -2490,7 +2546,7 @@ class GoogleAdsApi return $this -> aggregate_ad_groups( $results ); } - public function get_ad_groups_all_time( $customer_id ) + public function get_ad_groups_all_time( $customer_id, $as_of_date = null ) { $gaql = "SELECT " . "campaign.id, " @@ -2505,7 +2561,24 @@ class GoogleAdsApi . "WHERE campaign.status = 'ENABLED' " . "AND ad_group.status = 'ENABLED'"; - $results = $this -> search_stream( $customer_id, $gaql ); + $results = false; + if ( $as_of_date ) + { + $as_of_date = date( 'Y-m-d', strtotime( $as_of_date ) ); + $gaql_with_date = $gaql . " AND segments.date <= '" . $as_of_date . "'"; + $results = $this -> search_stream( $customer_id, $gaql_with_date ); + + // Fallback gdy konto nie akceptuje filtra daty na all-time. + if ( $results === false ) + { + $results = $this -> search_stream( $customer_id, $gaql ); + } + } + else + { + $results = $this -> search_stream( $customer_id, $gaql ); + } + if ( $results === false ) return false; return $this -> aggregate_ad_groups( $results ); @@ -2688,6 +2761,72 @@ class GoogleAdsApi return array_values( $scopes ); } + public function get_shopping_ad_group_offer_ids_from_shopping_product( $customer_id ) + { + $gaql = "SELECT " + . "campaign.id, " + . "campaign.name, " + . "campaign.status, " + . "campaign.advertising_channel_type, " + . "ad_group.id, " + . "ad_group.name, " + . "ad_group.status, " + . "shopping_product.item_id " + . "FROM shopping_product " + . "WHERE campaign.status != 'REMOVED' " + . "AND ad_group.status != 'REMOVED' " + . "AND campaign.advertising_channel_type = 'SHOPPING' " + . "AND segments.date DURING LAST_30_DAYS"; + + $results = $this -> search_stream( $customer_id, $gaql ); + if ( $results === false ) + { + return false; + } + + if ( !is_array( $results ) ) + { + return []; + } + + $scopes = []; + + foreach ( $results as $row ) + { + $campaign_id = (int) ( $row['campaign']['id'] ?? 0 ); + $ad_group_id = (int) ( $row['adGroup']['id'] ?? 0 ); + $offer_id = trim( (string) ( $row['shoppingProduct']['itemId'] ?? '' ) ); + + if ( $campaign_id <= 0 || $ad_group_id <= 0 || $offer_id === '' ) + { + continue; + } + + $scope_key = $campaign_id . '|' . $ad_group_id; + + if ( !isset( $scopes[ $scope_key ] ) ) + { + $scopes[ $scope_key ] = [ + 'campaign_id' => $campaign_id, + 'campaign_name' => trim( (string) ( $row['campaign']['name'] ?? '' ) ), + 'ad_group_id' => $ad_group_id, + 'ad_group_name' => trim( (string) ( $row['adGroup']['name'] ?? '' ) ), + 'offer_ids' => [] + ]; + } + + $scopes[ $scope_key ]['offer_ids'][ $offer_id ] = true; + } + + foreach ( $scopes as &$scope ) + { + $scope['offer_ids'] = array_values( array_keys( (array) $scope['offer_ids'] ) ); + } + unset( $scope ); + + return array_values( $scopes ); + } + public function get_shopping_ad_group_offer_ids_from_performance( $customer_id ) { $gaql = "SELECT " diff --git a/config.php b/config.php index faaebf3..ccf671c 100644 --- a/config.php +++ b/config.php @@ -12,5 +12,5 @@ $settings['email_password'] = 'ProjectPro2025!'; $settings['cron_products_clients_per_run'] = 1; $settings['cron_campaigns_clients_per_run'] = 1; -$settings['cron_products_urls_limit_per_client'] = 10; +$settings['cron_products_urls_limit_per_client'] = 100; $settings['google_ads_conversion_window_days'] = 7; diff --git a/docs/PLAN.md b/docs/PLAN.md deleted file mode 100644 index 45dbefb..0000000 --- a/docs/PLAN.md +++ /dev/null @@ -1,280 +0,0 @@ -# adsPRO - System Zarządzania Reklamami Google ADS & Facebook ADS - -## Opis projektu -adsPRO to narzędzie webowe (PHP) do zarządzania i automatyzacji kampanii reklamowych Google ADS (priorytet) oraz Facebook ADS (planowane). System umożliwia monitorowanie kampanii, zarządzanie produktami, analizę wydajności (ROAS, CTR, CPC) oraz automatyczne etykietowanie produktów (bestsellery, zombie itp.). - -**URL:** https://adspro.projectpro.pl -**Hosting:** Hostido (shared hosting) - -## Stack technologiczny -- **PHP 8.x** - czyste PHP z własną strukturą MVC (bez frameworka) -- **MySQL/MariaDB** - baza danych (Medoo ORM) -- **Google ADS API** - pobieranie danych kampanii i produktów (CRON) -- **Facebook ADS API** - planowane w przyszłości -- **CRON** - automatyczna synchronizacja danych -- **SCSS** - stylowanie (kompilacja do CSS) -- **jQuery 3.6** - interaktywność frontend -- **DataTables 2.1.7** - tabele z sortowaniem, filtrowaniem, paginacją -- **Highcharts** - wykresy wydajności -- **Select2** - zaawansowane selecty -- **Font Awesome** - ikony - -## Struktura katalogów (nowa) - -``` -public_html/ -├── index.php # Front controller + nowy router -├── .htaccess # Rewrite rules -├── .env # Konfiguracja (przyszłość - migracja z config.php) -├── config.php # Konfiguracja DB (obecna) -├── ajax.php # Ajax handler -├── api.php # API handler (Google ADS webhook) -├── cron.php # CRON handler -├── robots.txt -├── layout/ -│ ├── favicon.png -│ ├── style.scss # Główne style (SCSS) -│ └── style.css # Skompilowane style -├── libraries/ # Biblioteki zewnętrzne -│ ├── medoo/ -│ ├── phpmailer/ -│ ├── select2/ -│ ├── jquery-confirm/ -│ ├── functions.js # Globalne funkcje JS -│ └── framework/ # Framework UI (skin, pluginy) -├── autoload/ # Kod PHP (MVC) -│ ├── class.S.php # Helper: sesje, requesty, narzędzia -│ ├── class.Tpl.php # Template engine -│ ├── class.Cache.php # Cache -│ ├── class.DbModel.php # Bazowy model DB -│ ├── class.Html.php # HTML helper -│ ├── controls/ # Kontrolery -│ │ ├── class.Site.php # Router (do przebudowy) -│ │ ├── class.Users.php # Logowanie, ustawienia -│ │ ├── class.Dashboard.php # NOWY - Dashboard główny -│ │ ├── class.Campaigns.php # Kampanie Google ADS -│ │ ├── class.Products.php # Produkty -│ │ ├── class.Allegro.php # Import Allegro -│ │ ├── class.Reports.php # NOWY - Raporty i analityka -│ │ ├── class.Api.php # Endpointy API -│ │ └── class.Cron.php # CRON joby -│ ├── factory/ # Modele danych (DB queries) -│ │ ├── class.Users.php -│ │ ├── class.Campaigns.php -│ │ ├── class.Products.php -│ │ └── class.Cron.php -│ └── view/ # View helpers -│ ├── class.Site.php # Renderer layoutu -│ ├── class.Users.php -│ └── class.Cron.php -├── templates/ # Szablony PHP -│ ├── site/ -│ │ ├── layout-logged.php # Layout z sidebar (PRZEBUDOWA) -│ │ └── layout-unlogged.php # Layout logowania (PRZEBUDOWA) -│ ├── auth/ -│ │ └── login.php # NOWY ekran logowania -│ ├── dashboard/ -│ │ └── index.php # NOWY dashboard -│ ├── campaigns/ -│ │ └── main_view.php # Widok kampanii -│ ├── products/ -│ │ ├── main_view.php # Lista produktów -│ │ └── product_history.php # Historia produktu -│ ├── allegro/ -│ │ └── main_view.php # Import Allegro -│ ├── reports/ # NOWE -│ │ └── index.php # Raporty -│ ├── users/ -│ │ ├── login-form.php # Stary login (do usunięcia) -│ │ └── settings.php # Ustawienia użytkownika -│ └── html/ # Komponenty HTML -│ ├── button.php -│ ├── input.php -│ ├── select.php -│ └── ... -├── tools/ -│ └── google-taxonomy.php -├── tmp/ -└── docs/ - └── PLAN.md -``` - -## Nowy system routingu - -### Zasada działania -Zamiast obecnego `?module=X&action=Y` → czyste URLe obsługiwane przez `.htaccess` + nowy router w `class.Site.php`. - -### Mapa URL - -| URL | Kontroler | Metoda | Opis | -|-----|-----------|--------|------| -| `/login` | Users | login_form | Ekran logowania | -| `/logout` | Users | logout | Wylogowanie | -| `/` | Dashboard | index | Dashboard główny | -| `/campaigns` | Campaigns | main_view | Lista kampanii | -| `/campaigns/history/{id}` | Campaigns | history | Historia kampanii | -| `/products` | Products | main_view | Lista produktów | -| `/products/history/{id}` | Products | product_history | Historia produktu | -| `/allegro` | Allegro | main_view | Import Allegro | -| `/reports` | Reports | index | Raporty | -| `/settings` | Users | settings | Ustawienia konta | -| `/api/*` | Api | * | Endpointy API | -| `/cron/*` | Cron | * | CRON joby | - -### Nowy .htaccess -```apache -RewriteEngine On -RewriteBase / - -# Statyczne zasoby - pomijaj -RewriteCond %{REQUEST_URI} ^/(libraries|layout|upload|temp)/ [NC] -RewriteRule ^ - [L] - -# Wszystko inne → index.php -RewriteCond %{REQUEST_FILENAME} !-f -RewriteCond %{REQUEST_FILENAME} !-d -RewriteRule ^(.*)$ index.php [L,QSA] -``` - -### Nowy router (class.Site.php) -```php -// Parsowanie URL z $_SERVER['REQUEST_URI'] -// Mapowanie: /segment1/segment2/segment3 → kontroler/akcja/parametry -// Fallback na dashboard dla zalogowanych, login dla niezalogowanych -``` - -## Główne funkcje - -### 1. Nowy ekran logowania -- Nowoczesny design: podzielony ekran (lewa strona - branding/grafika, prawa - formularz) -- Logo "adsPRO" z subtitlem -- Pola: email + hasło -- Checkbox "Zapamiętaj mnie" -- Walidacja AJAX -- Animacje przejścia -- Responsywność (mobile: tylko formularz) - -### 2. Nowy layout z menu bocznym (sidebar) -- **Sidebar (lewa strona, 260px):** - - Logo "adsPRO" na górze - - Menu nawigacyjne z ikonami Font Awesome: - - 📊 Dashboard (`/`) - - 📢 Kampanie (`/campaigns`) - - 📦 Produkty (`/products`) - - 📥 Allegro import (`/allegro`) - - 📈 Raporty (`/reports`) - - ⚙️ Ustawienia (`/settings`) - - Aktywny element podświetlony - - Możliwość zwijania sidebar (collapsed → same ikony, 60px) - - Na dole: info o zalogowanym użytkowniku + przycisk wylogowania -- **Top bar (nad contentem):** - - Przycisk hamburger (toggle sidebar) - - Breadcrumbs (ścieżka nawigacji) - - Szybkie akcje / notyfikacje (przyszłość) -- **Content area:** - - Pełna szerokość minus sidebar - - Padding 25px - - Tło #F4F6F9 (jaśniejsze od obecnego) - -### 3. Dashboard (NOWY) -- Kafelki podsumowujące (karty): - - Łączna liczba kampanii - - Łączna liczba produktów - - Średni ROAS (30 dni) - - Łączne wydatki (30 dni) -- Wykres trendu ROAS (ostatnie 30 dni) -- Lista ostatnio zmodyfikowanych kampanii -- Produkty wymagające uwagi (niski ROAS, zombie) - -### 4. Zarządzanie kampaniami Google ADS -- Wybór klienta (select) -- Lista kampanii z metrykami (DataTables) -- Historia kampanii z wykresem Highcharts -- Metryki: ROAS, budżet, wydatki, wartość konwersji, strategia bidding -- Usuwanie kampanii i wpisów historii -- Komentarze do kampanii - -### 5. Zarządzanie produktami -- Wybór klienta -- Konfiguracja min. ROAS dla bestsellerów -- Tabela produktów z metrykami: - - Wyświetlenia, kliknięcia, CTR, koszt, CPC - - Konwersje, wartość konwersji, ROAS - - Custom labels (bestseller/zombie/deleted/pla/paused) -- Edycja inline (min_roas, custom_label) -- Edycja produktu w modalu (tytuł, opis, kategoria Google) -- Historia produktu z wykresem -- Bulk delete zaznaczonych produktów - -### 6. Import Allegro -- Upload pliku CSV -- Automatyczne mapowanie ofert -- Raport importu (dodane, zaktualizowane) - -### 7. Raporty (NOWY - przyszłość) -- Raport wydajności kampanii -- Raport produktów (bestsellery vs zombie) -- Eksport do Excel -- Porównanie okresów - -### 8. Ustawienia -- Dane konta (email) -- Zmiana hasła -- Konfiguracja Pushover (powiadomienia) -- Klucze API (przyszłość: Google ADS, Facebook ADS) - -### 9. CRON - synchronizacja danych -- `cron_products` - synchronizacja produktów z Google ADS -- `cron_products_history_30` - historia 30-dniowa produktów -- `cron_xml` - generowanie XML -- `cron_phrases` - synchronizacja fraz -- `cron_phrases_history_30` - historia 30-dniowa fraz - -## Plan implementacji - -| Etap | Zakres | Priorytet | Pliki | -|------|--------|-----------|-------| -| **1. Nowy routing** | Przebudowa routera, nowy .htaccess, parsowanie czystych URL | 🔴 Wysoki | `.htaccess`, `index.php`, `controls/class.Site.php` | -| **2. Nowy layout (sidebar)** | Layout z bocznym menu, top bar, responsywność | 🔴 Wysoki | `templates/site/layout-logged.php`, `layout/style.scss` | -| **3. Nowy ekran logowania** | Nowoczesny split-screen login, nowy layout-unlogged | 🔴 Wysoki | `templates/site/layout-unlogged.php`, `templates/auth/login.php`, `layout/style.scss` | -| **4. Dashboard** | Nowa strona startowa z podsumowaniem | 🟡 Średni | `controls/class.Dashboard.php`, `templates/dashboard/index.php` | -| **5. Migracja kampanii** | Dostosowanie widoku kampanii do nowego routingu | 🟡 Średni | `controls/class.Campaigns.php`, `templates/campaigns/*` | -| **6. Migracja produktów** | Dostosowanie widoku produktów do nowego routingu | 🟡 Średni | `controls/class.Products.php`, `templates/products/*` | -| **7. Migracja Allegro** | Dostosowanie importu Allegro | 🟢 Niski | `controls/class.Allegro.php`, `templates/allegro/*` | -| **8. Moduł raportów** | Nowy moduł analityczny | 🟢 Niski | `controls/class.Reports.php`, `templates/reports/*` | -| **9. Facebook ADS** | Integracja z Facebook ADS API | 🔵 Przyszłość | Nowe kontrolery, factory, szablony | - -## Kolorystyka i design - -### Paleta kolorów -- **Primary (akcent):** `#6690F4` (niebieski - obecny) -- **Sidebar tło:** `#1E2A3A` (ciemny granat) -- **Sidebar tekst:** `#A8B7C7` (jasny szary) -- **Sidebar active:** `#6690F4` (primary) -- **Content tło:** `#F4F6F9` (jasnoszary) -- **Karty:** `#FFFFFF` -- **Tekst:** `#4E5E6A` (obecny) -- **Success:** `#57B951` -- **Danger:** `#CC0000` -- **Warning:** `#FF8C00` - -### Typografia -- Font: Open Sans (obecny - zachowany) -- Rozmiar bazowy: 14px (sidebar), 15px (content) - -## Bezpieczeństwo -- Hasła hashowane MD5 (obecne) → **TODO: migracja na bcrypt** -- Sesje PHP + cookie "zapamiętaj mnie" -- Prepared statements (Medoo ORM) -- htmlspecialchars() w szablonach -- **TODO: CSRF tokeny w formularzach** -- **TODO: migracja config.php → .env (z .htaccess deny)** - -## Przyszłe rozszerzenia -- Facebook ADS API - zarządzanie kampaniami FB -- System powiadomień (Pushover + in-app) -- Wielojęzyczność (PL/EN) -- Role użytkowników (admin, manager, viewer) -- Automatyczne reguły (np. "jeśli ROAS < X → zmień label na zombie") -- Integracja z Google Merchant Center -- API REST do integracji z innymi systemami diff --git a/docs/database.sql b/docs/database.sql index 83931ff..f800ac5 100644 --- a/docs/database.sql +++ b/docs/database.sql @@ -1,44 +1,24 @@ --- -------------------------------------------------------- --- Host: host700513.hostido.net.pl --- Wersja serwera: 10.11.15-MariaDB-cll-lve - MariaDB Server --- Serwer OS: Linux --- HeidiSQL Wersja: 12.6.0.6765 --- -------------------------------------------------------- +-- Zrzut struktury tabela host700513_adspro.clients +CREATE TABLE IF NOT EXISTS `clients` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL DEFAULT '0', + `google_ads_customer_id` varchar(20) DEFAULT NULL, + `google_merchant_account_id` varchar(32) DEFAULT NULL, + `google_ads_start_date` date DEFAULT NULL, + `active` int(11) DEFAULT 0, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -/*!40101 SET NAMES utf8 */; -/*!50503 SET NAMES utf8mb4 */; -/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; -/*!40103 SET TIME_ZONE='+00:00' */; -/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; -/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; -/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; - --- Zrzut struktury tabela host700513_adspro.campaigns CREATE TABLE IF NOT EXISTS `campaigns` ( `id` int(11) NOT NULL AUTO_INCREMENT, `client_id` int(11) NOT NULL DEFAULT 0, `campaign_id` bigint(20) NOT NULL DEFAULT 0, `campaign_name` varchar(255) NOT NULL DEFAULT '0', + `advertising_channel_type` varchar(40) DEFAULT NULL, PRIMARY KEY (`id`), KEY `client_id` (`client_id`), CONSTRAINT `FK__clients` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=123 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.campaigns_comments -CREATE TABLE IF NOT EXISTS `campaigns_comments` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `campaign_id` int(11) NOT NULL, - `comment` text NOT NULL, - `date_add` date NOT NULL DEFAULT current_timestamp(), - PRIMARY KEY (`id`) USING BTREE, - KEY `campaign_id` (`campaign_id`), - CONSTRAINT `FK_campaigns_comments_campaigns` FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; - --- Eksport danych został odznaczony. +) ENGINE=InnoDB AUTO_INCREMENT=56 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Zrzut struktury tabela host700513_adspro.campaigns_history CREATE TABLE IF NOT EXISTS `campaigns_history` ( @@ -54,235 +34,7 @@ CREATE TABLE IF NOT EXISTS `campaigns_history` ( PRIMARY KEY (`id`) USING BTREE, KEY `offer_id` (`campaign_id`) USING BTREE, CONSTRAINT `FK_campaigns_history_campaigns` FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=4400 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.clients -CREATE TABLE IF NOT EXISTS `clients` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `name` varchar(255) NOT NULL DEFAULT '0', - `google_ads_customer_id` varchar(20) DEFAULT NULL, - `google_merchant_account_id` varchar(32) DEFAULT NULL, - `google_ads_start_date` date DEFAULT NULL, - `deleted` int(11) DEFAULT 0, - `bestseller_min_roas` int(11) DEFAULT NULL, - PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=46 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.phrases -CREATE TABLE IF NOT EXISTS `phrases` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `client_id` int(11) NOT NULL DEFAULT 0, - `phrase` varchar(255) NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - KEY `FK_phrases_clients` (`client_id`), - CONSTRAINT `FK_phrases_clients` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=5512 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.phrases_history -CREATE TABLE IF NOT EXISTS `phrases_history` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `phrase_id` int(11) NOT NULL DEFAULT 0, - `impressions` int(11) NOT NULL DEFAULT 0, - `clicks` int(11) NOT NULL DEFAULT 0, - `cost` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversions` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversions_value` decimal(20,6) NOT NULL DEFAULT 0.000000, - `date_add` date NOT NULL DEFAULT '0000-00-00', - `updated` int(11) NOT NULL DEFAULT 0, - `deleted` int(11) DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - KEY `offer_id` (`phrase_id`) USING BTREE, - CONSTRAINT `FK_phrases_history_phrases` FOREIGN KEY (`phrase_id`) REFERENCES `phrases` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=13088 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.phrases_history_30 -CREATE TABLE IF NOT EXISTS `phrases_history_30` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `phrase_id` int(11) NOT NULL, - `impressions` int(11) NOT NULL, - `clicks` int(11) NOT NULL, - `cost` decimal(20,6) NOT NULL, - `conversions` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversions_value` decimal(20,6) NOT NULL, - `roas` decimal(20,6) NOT NULL, - `date_add` date NOT NULL DEFAULT '0000-00-00', - `deleted` int(11) DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - KEY `offer_id` (`phrase_id`) USING BTREE, - CONSTRAINT `FK_phrases_history_30_phrases` FOREIGN KEY (`phrase_id`) REFERENCES `phrases` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=1795 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.phrases_temp -CREATE TABLE IF NOT EXISTS `phrases_temp` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `phrase_id` int(11) DEFAULT NULL, - `phrase` varchar(255) DEFAULT NULL, - `impressions` int(11) DEFAULT NULL, - `clicks` int(11) DEFAULT NULL, - `cost` decimal(20,6) DEFAULT NULL, - `conversions` decimal(20,6) DEFAULT NULL, - `conversions_value` decimal(20,6) DEFAULT NULL, - `cpc` decimal(20,6) DEFAULT NULL, - `roas` decimal(20,0) DEFAULT NULL, - PRIMARY KEY (`id`) USING BTREE, - KEY `offer_id` (`phrase_id`) USING BTREE, - CONSTRAINT `FK_phrases_temp_phrases` FOREIGN KEY (`phrase_id`) REFERENCES `phrases` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=353973 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.products -CREATE TABLE IF NOT EXISTS `products` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `client_id` int(11) NOT NULL DEFAULT 0, - `offer_id` varchar(50) NOT NULL DEFAULT '0', - `name` varchar(255) NOT NULL DEFAULT '0', - `min_roas` int(11) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `FK_offers_clients` (`client_id`), - CONSTRAINT `FK_offers_clients` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=5927 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.products_comments -CREATE TABLE IF NOT EXISTS `products_comments` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `product_id` int(11) NOT NULL, - `comment` text NOT NULL, - `date_add` date NOT NULL DEFAULT current_timestamp(), - PRIMARY KEY (`id`), - KEY `product_id` (`product_id`) USING BTREE, - CONSTRAINT `FK_products_comments_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=118 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.products_data -CREATE TABLE IF NOT EXISTS `products_data` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `product_id` int(11) DEFAULT NULL, - `custom_label_4` varchar(255) DEFAULT NULL, - `custom_label_3` varchar(255) DEFAULT NULL, - `title` varchar(255) DEFAULT NULL, - `description` text DEFAULT NULL, - `google_product_category` text DEFAULT NULL, - `product_url` varchar(500) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `product_id` (`product_id`) USING BTREE, - CONSTRAINT `FK_products_data_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=118 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.products_history -CREATE TABLE IF NOT EXISTS `products_history` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `product_id` int(11) NOT NULL DEFAULT 0, - `impressions` int(11) NOT NULL DEFAULT 0, - `clicks` int(11) NOT NULL DEFAULT 0, - `ctr` decimal(20,6) NOT NULL DEFAULT 0.000000, - `cost` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversions` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversions_value` decimal(20,6) NOT NULL DEFAULT 0.000000, - `date_add` date NOT NULL DEFAULT '0000-00-00', - `updated` int(11) NOT NULL DEFAULT 0, - `deleted` int(11) DEFAULT 0, - PRIMARY KEY (`id`) USING BTREE, - KEY `product_id` (`product_id`) USING BTREE, - CONSTRAINT `FK_products_history_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=63549 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.products_history_30 -CREATE TABLE IF NOT EXISTS `products_history_30` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `product_id` int(11) NOT NULL, - `impressions` int(11) NOT NULL, - `clicks` int(11) NOT NULL, - `ctr` decimal(20,6) NOT NULL DEFAULT 0.000000, - `cost` decimal(20,6) NOT NULL, - `conversions` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversions_value` decimal(20,6) NOT NULL, - `roas` decimal(20,6) NOT NULL, - `roas_all_time` decimal(20,6) NOT NULL, - `date_add` date NOT NULL DEFAULT '0000-00-00', - `deleted` int(11) DEFAULT 0, - PRIMARY KEY (`id`), - KEY `product_id` (`product_id`) USING BTREE, - CONSTRAINT `FK_products_history_30_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=27655 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.products_temp -CREATE TABLE IF NOT EXISTS `products_temp` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `product_id` int(11) DEFAULT NULL, - `name` varchar(255) DEFAULT NULL, - `impressions` int(11) DEFAULT NULL, - `impressions_30` int(11) DEFAULT NULL, - `clicks` int(11) DEFAULT NULL, - `clicks_30` int(11) DEFAULT NULL, - `ctr` decimal(20,6) DEFAULT NULL, - `cost` decimal(20,6) DEFAULT NULL, - `conversions` decimal(20,6) DEFAULT NULL, - `conversions_value` decimal(20,6) DEFAULT NULL, - `cpc` decimal(20,6) DEFAULT NULL, - `roas` decimal(20,0) DEFAULT NULL, - PRIMARY KEY (`id`), - KEY `product_id` (`product_id`) USING BTREE, - CONSTRAINT `FK_products_temp_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE -) ENGINE=InnoDB AUTO_INCREMENT=298845 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.settings -CREATE TABLE IF NOT EXISTS `settings` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `setting_key` varchar(100) NOT NULL, - `setting_value` text DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `uk_setting_key` (`setting_key`) -) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci; - --- Eksport danych został odznaczony. - --- Zrzut struktury tabela host700513_adspro.users -CREATE TABLE IF NOT EXISTS `users` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `email` varchar(255) NOT NULL, - `password` varchar(255) NOT NULL, - `name` varchar(255) DEFAULT NULL, - `surname` varchar(255) DEFAULT NULL, - `default_project` int(11) DEFAULT NULL, - `color` varchar(50) DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `email` (`email`) -) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_polish_ci; - --- Eksport danych został odznaczony. - -/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */; -/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */; -/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */; -/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; -/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */; - --- ================================ --- DODANE: struktury kampanie > grupy/frazy --- ================================ +) ENGINE=InnoDB AUTO_INCREMENT=381 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; CREATE TABLE IF NOT EXISTS `campaign_ad_groups` ( `id` int(11) NOT NULL AUTO_INCREMENT, @@ -304,67 +56,122 @@ CREATE TABLE IF NOT EXISTS `campaign_ad_groups` ( `date_sync` date DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_campaign_ad_groups_campaign_ad_group` (`campaign_id`,`ad_group_id`), - KEY `idx_campaign_ad_groups_campaign_id` (`campaign_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + KEY `idx_campaign_ad_groups_campaign_id` (`campaign_id`), + CONSTRAINT `FK_campaign_ad_groups_campaigns` FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=125 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -CREATE TABLE IF NOT EXISTS `campaign_search_terms` ( +CREATE TABLE IF NOT EXISTS `campaign_alerts` ( `id` int(11) NOT NULL AUTO_INCREMENT, - `campaign_id` int(11) NOT NULL, - `ad_group_id` int(11) NOT NULL, - `search_term` varchar(255) NOT NULL, - `impressions_30` int(11) NOT NULL DEFAULT 0, - `clicks_30` int(11) NOT NULL DEFAULT 0, - `cost_30` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversions_30` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversion_value_30` decimal(20,6) NOT NULL DEFAULT 0.000000, - `roas_30` decimal(20,6) NOT NULL DEFAULT 0.000000, - `impressions_all_time` int(11) NOT NULL DEFAULT 0, - `clicks_all_time` int(11) NOT NULL DEFAULT 0, - `cost_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversions_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversion_value_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000, - `roas_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000, - `date_sync` date DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `uk_campaign_search_terms` (`campaign_id`,`ad_group_id`,`search_term`), - KEY `idx_campaign_search_terms_campaign_id` (`campaign_id`), - KEY `idx_campaign_search_terms_ad_group_id` (`ad_group_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -CREATE TABLE IF NOT EXISTS `campaign_keywords` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `campaign_id` int(11) NOT NULL, - `ad_group_id` int(11) NOT NULL, - `keyword_text` varchar(255) NOT NULL, - `match_type` varchar(40) DEFAULT NULL, - `impressions_30` int(11) NOT NULL DEFAULT 0, - `clicks_30` int(11) NOT NULL DEFAULT 0, - `cost_30` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversions_30` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversion_value_30` decimal(20,6) NOT NULL DEFAULT 0.000000, - `roas_30` decimal(20,6) NOT NULL DEFAULT 0.000000, - `impressions_all_time` int(11) NOT NULL DEFAULT 0, - `clicks_all_time` int(11) NOT NULL DEFAULT 0, - `cost_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversions_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000, - `conversion_value_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000, - `roas_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000, - `date_sync` date DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `uk_campaign_keywords` (`campaign_id`,`ad_group_id`,`keyword_text`(191),`match_type`), - KEY `idx_campaign_keywords_campaign_id` (`campaign_id`), - KEY `idx_campaign_keywords_ad_group_id` (`ad_group_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; - -CREATE TABLE IF NOT EXISTS `campaign_negative_keywords` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `campaign_id` int(11) NOT NULL, + `client_id` int(11) NOT NULL, + `campaign_id` int(11) DEFAULT NULL, + `campaign_external_id` bigint(20) DEFAULT NULL, `ad_group_id` int(11) DEFAULT NULL, - `scope` varchar(20) NOT NULL DEFAULT 'campaign', - `keyword_text` varchar(255) NOT NULL, - `match_type` varchar(40) DEFAULT NULL, - `date_sync` date DEFAULT NULL, + `ad_group_external_id` bigint(20) DEFAULT NULL, + `product_id` int(11) DEFAULT NULL, + `alert_type` varchar(120) NOT NULL, + `message` text NOT NULL, + `meta_json` text DEFAULT NULL, + `date_detected` date NOT NULL, + `date_add` datetime NOT NULL DEFAULT current_timestamp(), + `unseen` tinyint(1) NOT NULL DEFAULT 1, PRIMARY KEY (`id`), - KEY `idx_campaign_negative_keywords_campaign_id` (`campaign_id`), - KEY `idx_campaign_negative_keywords_ad_group_id` (`ad_group_id`) + UNIQUE KEY `uniq_alert_daily` (`client_id`,`campaign_external_id`,`ad_group_external_id`,`alert_type`,`date_detected`), + KEY `idx_alert_date` (`date_detected`), + KEY `idx_alert_client` (`client_id`), + KEY `idx_alert_campaign` (`campaign_id`), + KEY `idx_alert_ad_group` (`ad_group_id`), + KEY `idx_alert_unseen` (`unseen`), + KEY `idx_alert_product` (`product_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; + +CREATE TABLE IF NOT EXISTS `products` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `client_id` int(11) NOT NULL DEFAULT 0, + `offer_id` varchar(50) NOT NULL DEFAULT '0', + `name` varchar(255) NOT NULL DEFAULT '0', + `min_roas` int(11) DEFAULT NULL, + `custom_label_4` varchar(255) DEFAULT NULL, + `custom_label_3` varchar(255) DEFAULT NULL, + `title` varchar(255) DEFAULT NULL, + `description` text DEFAULT NULL, + `google_product_category` text DEFAULT NULL, + `product_url` varchar(500) DEFAULT NULL, + `merchant_url_not_found` tinyint(1) NOT NULL DEFAULT 0, + `merchant_url_last_check` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `FK_offers_clients` (`client_id`), + CONSTRAINT `FK_offers_clients` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=8482 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `products_history` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `product_id` int(11) NOT NULL DEFAULT 0, + `campaign_id` int(11) NOT NULL DEFAULT 0, + `ad_group_id` int(11) NOT NULL DEFAULT 0, + `impressions` int(11) NOT NULL DEFAULT 0, + `clicks` int(11) NOT NULL DEFAULT 0, + `ctr` decimal(20,6) NOT NULL DEFAULT 0.000000, + `cost` decimal(20,6) NOT NULL DEFAULT 0.000000, + `conversions` decimal(20,6) NOT NULL DEFAULT 0.000000, + `conversions_value` decimal(20,6) NOT NULL DEFAULT 0.000000, + `date_add` date NOT NULL DEFAULT '0000-00-00', + `updated` int(11) NOT NULL DEFAULT 0, + `deleted` int(11) DEFAULT 0, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_products_history_scope_day` (`product_id`,`campaign_id`,`ad_group_id`,`date_add`), + KEY `product_id` (`product_id`) USING BTREE, + KEY `idx_products_history_campaign_id` (`campaign_id`), + KEY `idx_products_history_ad_group_id` (`ad_group_id`), + CONSTRAINT `FK_products_history_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=37033 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; + +CREATE TABLE IF NOT EXISTS `products_history_30` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `product_id` int(11) NOT NULL, + `campaign_id` int(11) NOT NULL DEFAULT 0, + `ad_group_id` int(11) NOT NULL DEFAULT 0, + `impressions` int(11) NOT NULL, + `clicks` int(11) NOT NULL, + `ctr` decimal(20,6) NOT NULL DEFAULT 0.000000, + `cost` decimal(20,6) NOT NULL, + `conversions` decimal(20,6) NOT NULL DEFAULT 0.000000, + `conversions_value` decimal(20,6) NOT NULL, + `roas` decimal(20,6) NOT NULL, + `roas_all_time` decimal(20,6) NOT NULL, + `date_add` date NOT NULL DEFAULT '0000-00-00', + `deleted` int(11) DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_products_history_30_scope_day` (`product_id`,`campaign_id`,`ad_group_id`,`date_add`), + KEY `product_id` (`product_id`) USING BTREE, + KEY `idx_products_history_30_campaign_id` (`campaign_id`), + KEY `idx_products_history_30_ad_group_id` (`ad_group_id`), + CONSTRAINT `FK_products_history_30_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS `products_aggregate` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `product_id` int(11) NOT NULL, + `campaign_id` int(11) NOT NULL DEFAULT 0, + `ad_group_id` int(11) NOT NULL DEFAULT 0, + `impressions_30` int(11) NOT NULL DEFAULT 0, + `clicks_30` int(11) NOT NULL DEFAULT 0, + `ctr_30` decimal(20,6) NOT NULL DEFAULT 0.000000, + `cost_30` decimal(20,6) NOT NULL DEFAULT 0.000000, + `conversions_30` decimal(20,6) NOT NULL DEFAULT 0.000000, + `conversion_value_30` decimal(20,6) NOT NULL DEFAULT 0.000000, + `roas_30` decimal(20,6) NOT NULL DEFAULT 0.000000, + `impressions_all_time` int(11) NOT NULL DEFAULT 0, + `clicks_all_time` int(11) NOT NULL DEFAULT 0, + `ctr_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000, + `cost_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000, + `conversions_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000, + `conversion_value_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000, + `roas_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000, + `date_sync` date NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_products_aggregate_scope` (`product_id`,`campaign_id`,`ad_group_id`), + KEY `idx_products_aggregate_campaign_id` (`campaign_id`), + KEY `idx_products_aggregate_ad_group_id` (`ad_group_id`), + KEY `idx_products_aggregate_date_sync` (`date_sync`), + CONSTRAINT `FK_products_aggregate_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/docs/memory.md b/docs/memory.md index 231867e..33c2629 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -1,55 +1,397 @@ -# adsPRO - Pamiec projektu +# 2026-02-20 - Obsluga statusu ACTIVE dla klientow -Ten plik sluzy jako trwala pamiec dla Claude Code. Zapisuj tu wzorce, decyzje i ustalenia potwierdzone w trakcie pracy nad projektem. +## Zmienione pliki -## Architektura +- `autoload/controls/class.Clients.php` + - `save()` zapisuje teraz pole `active` (domyslnie `1`, gdy brak wartosci z formularza). + - Dodana nowa akcja `set_active()` pod endpoint `/clients/set_active` do szybkiej zmiany statusu klienta AJAX-em. + - `force_sync()` ma dodatkowa walidacje: + - nie pozwala kolejkowac synchronizacji dla klienta nieaktywnego (`active != 1`), + - nadal blokuje klienta usunietego (`deleted = 1`) i klienta bez wymaganych ID. + - Kompatybilnosc schematu `clients` bez kolumny `deleted`: + - helpery `clients_has_deleted_column()` i `sql_clients_not_deleted()`, + - `force_sync()` i `sync_status()` nie wywalaja sie, gdy w bazie nie ma kolumny `deleted`. -- Custom MVC: Controllers (`\controls`) -> Factories (`\factory`) -> Medoo ORM (`$mdb`) -- Autoload PSR-0: `\controls\Foo` -> `autoload/controls/class.Foo.php` -- Routing w `index.php`: URL `/module/action/` -> `\controls\Module::action()` -- Szablony w `templates/`, zmienne przez `$this->varName` -- Serwisy API: `\services\GoogleAdsApi`, `\services\ClaudeApi`, `\services\OpenAiApi` +- `templates/clients/main_view.php` + - Tabela klientow ma nowa kolumne `Status` (Aktywny/Nieaktywny). + - Wiersz klienta trzyma `data-active` do obslugi UI i synchronizacji. + - Dodany przycisk toggle (ikona `fa-toggle-on/off`) do natychmiastowej aktywacji/dezaktywacji. + - Przyciski synchronizacji (kampanie/produkty/merchant) sa blokowane (`disabled`) dla nieaktywnego klienta i odblokowywane po aktywacji. + - Formularz Dodaj/Edytuj klienta ma nowe pole `Status klienta` (`active`). + - JS: + - `toggleClientActive()` wysyla POST na `/clients/set_active`, + - `updateClientStatusUI()` odswieza status i stan komorki Sync bez przeladowania strony, + - `loadSyncStatus()` pomija paski postepu dla nieaktywnych klientow i pokazuje `nieaktywny`. -## Styl kodu +## Gdzie to jest wykorzystywane -- Spacje w nawiasach: `if ( $x )`, `function( $a, $b )` -- Klamry w nowej linii -- Wszystkie metody kontrolerow i fabryk: `static public function` -- Endpointy JSON: `echo json_encode([...]); exit;` -- Commity po polsku z prefixem: `feat:`, `fix:`, `update:` +- Zarzadzanie statusem klienta: + - UI listy i formularza: `templates/clients/main_view.php` + - Backend zapisu i toggle: `autoload/controls/class.Clients.php` +- Ograniczenie recznego wymuszenia synchronizacji do klientow aktywnych: + - `autoload/controls/class.Clients.php` (`force_sync()`) -## Frontend +# 2026-02-20 - CRON kampanii (nowy przebieg, stare jako archiwum) -- jQuery 3.6, DataTables 2.1, Bootstrap 4, Select2 4.1, Highcharts -- jquery-confirm do modali/dialogow -- Font Awesome 6.5 do ikon -- SASS: `layout/style.scss` -> auto-kompilacja przez Live Sass Compiler +## Zmienione pliki -## Deployment +- `autoload/controls/class.Cron.php` + - Dodany nowy `cron_campaigns()` jako glowny endpoint pod nowy przeplyw. + - Stary kod zostal zachowany jako archiwum: `cron_campaigns_archive()`. + - Nowy przebieg: + - bierze tylko aktywnych klientow (`active = 1`) z Google Ads Customer ID, + - liczy okno dat na podstawie `google_ads_conversion_window_days` z `config.php` (z fallbackiem), + - konczy okno na `przedwczoraj` (bez pobierania danych dzisiejszych), + - przechodzi po datach dzien po dniu (rosnaco), + - zapisuje/aktualizuje kampanie do `campaigns`, + - zapisuje/aktualizuje historie dzienne do `campaigns_history` (upsert po `campaign_id + date_add`), + - zapisuje grupy reklam / groupy PMAX do `campaign_ad_groups`. + - po zakonczeniu kampanii + ad groups dla klienta, dla calego okna dat pobiera search terms dzienne do `campaign_search_terms_history`, + - po pobraniu historii search terms wykonuje agregacje do `campaign_search_terms` (zanim przejdzie do kolejnego klienta). + - Dodany krok syncu fraz dodanych i wykluczonych: + - tabele docelowe: `campaign_keywords` i `campaign_negative_keywords`, + - uruchamiany raz na cykl klienta (po ostatnim dniu okna), nie x razy dla kazdego dnia. +- Kampanie produktowe / PMAX: + - nie maja fraz dodanych, wiec w `campaign_keywords` moga miec 0 rekordow, + - frazy wykluczone sa dalej synchronizowane do `campaign_negative_keywords`. -- FTP auto-upload przez VS Code FTP-Kr -- Brak kroku budowania - pliki laduja bezposrednio na serwer -- Migracje: `php install.php` (idempotentne, sledzenie w `schema_migrations`) +# 2026-02-20 - Produkty: przygotowanie schematu bazy -## Decyzje projektowe +## Zmienione pliki -- Frazy wyszukiwane dodane do wykluczonych oznaczane czerwonym kolorem (klasa CSS `term-is-negative`) -- Negatywne slowa kluczowe dodawane przez Google Ads API i zapisywane lokalnie w `campaign_negative_keywords` -- Klucze API przechowywane w tabeli `settings` (key-value) -- Frazy z Google Ads Keyword Planner dla URL produktu sa cachowane w `products_keyword_planner_terms` i ponownie uzywane przy generowaniu tytulu AI -- Zmiany produktowe (`title`, `description`, `google_product_category`, `custom_label_4`) sa synchronizowane bezposrednio do Merchant API i logowane per pole w `products_merchant_sync_log` -- CRON dziala w trybie **klient po kliencie** (client-first): konczy WSZYSTKIE daty jednego klienta, potem przechodzi do nastepnego. Dzieki temu paski postepu na `/clients` roznia sie miedzy klientami. -- `cron_products` iteruje po datach per klient (`dates_per_run` z parametru `clients_per_run`), domyslnie `10` (max `100`); faza `aggregate_30` wywoluje `rebuild_products_temp` RAZ per klient -- `cron_campaigns` iteruje po datach per klient (`dates_per_run` z parametru `clients_per_run`), domyslnie `2` (max `20`) -- Helpery: `get_active_client($pipeline)` -> pierwszy klient z niezakonczona praca; `get_pending_dates_for_client()` -> daty do przetworzenia; `determine_client_products_phase()` -> faza per klient -- Stan CRON przechowywany w tabeli `cron_sync_status` (wiersz = klient + pipeline + data + phase), zamiast JSON w `settings` (migracja 012) -- Fazy produktow w `cron_sync_status`: pending -> fetch -> aggregate_30 -> done; kampanie: pending -> done -- Force sync klienta = DELETE z `cron_sync_status` (wiersze odtwarzane przez `ensure_sync_rows` w nastepnym cyklu CRON) -- Nowy klient/usuniety klient obslugiwany naturalnie: `ensure_sync_rows` dodaje brakujace, JOIN z `clients` pomija usunietych -- `cleanup_old_sync_rows(30)` czysci zakonczone wiersze starsze niz 30 dni i wiersze usunietych klientow +- `migrations/016_products_model_unification.sql` + - Dodane kolumny produktowe bezposrednio do `products`: + - `custom_label_4`, `custom_label_3`, `title`, `description`, `google_product_category`, `product_url`. + - Backfill danych z `products_data` -> `products` (tylko gdy pole w `products` jest puste). + - Dodana nowa tabela agregacyjna `products_aggregate`: + - scope: `product_id + campaign_id + ad_group_id` (unikalne), + - metryki `*_30` i `*_all_time`, + - `date_sync` (kiedy agregat byl przeliczony). -## Preferencje uzytkownika +- `docs/database.sql` + - Zaktualizowana definicja `products` o nowe kolumny danych produktu. + - Dodana definicja tabeli `products_aggregate`. -- Komunikacja po polsku -- Zwiezle commity po polsku -- Git push tylko na wyrazna prosbe +## Ustalenie projektowe + +- `products` staje sie glowna tabela danych produktu. +- `products_data` zostaje tymczasowo dla kompatybilnosci starego kodu; dane sa migrowane do `products`. +- Agregaty dla widokow `/products` powinny docelowo byc czytane z `products_aggregate` zamiast liczenia w locie. + +# 2026-02-20 - Produkty: przepiecie na `products` + agregaty + +## Zmienione pliki + +- `autoload/factory/class.Products.php` + - `get_product_data()`: + - najpierw czyta pola produktowe z `products` (`custom_label_4`, `custom_label_3`, `title`, `description`, `google_product_category`, `product_url`), + - fallback do `products_data` dla kompatybilnosci. + - `set_product_data()`: + - zapisuje pole glownie do `products`, + - rownolegle mirroruje zapis do `products_data` (kompatybilnosc starego kodu). + +- `autoload/controls/class.Cron.php` + - `sync_products_fetch_for_client()`: + - import produktow zapisuje dane produktowe bezposrednio do `products` (w tym `title`, `product_url`), + - usuniete poleganie na `products_data` podczas samego fetchu. + - `aggregate_products_history_30_for_client()`: + - po przeliczeniu `products_history_30` odpala przebudowe agregatow `products_aggregate` dla klienta i dnia. + - Dodana metoda `rebuild_products_aggregate_for_client( $client_id, $date_sync )`: + - liczy metryki `*_30` i `*_all_time` z `products_history`, + - zapisuje scope (`product_id + campaign_id + ad_group_id`) do `products_aggregate`. + - `rebuild_products_temp_for_client()`: + - przestawione z liczenia bezposrednio po `products_history` na odczyt z `products_aggregate`, + - zmniejsza liczenie "w locie" dla widoku `/products`. + - `cron_product_history_30_save()`: + - `products_history_30` przechowuje teraz srednie dzienne wartosci z okna do 30 dni (zamiast sumy okna), + - nadal zapisuje `roas_all_time` dla danego dnia. + - `generate_custom_feed_for_client()`: + - zrodlo danych produktowych przepiete na `products` (bez wymaganego `INNER JOIN products_data`). + - diagnostyka i pobieranie brakujacych URL (`cron_products_urls`): + - logika "ma URL / brak URL" bierze pod uwage `products.product_url` z fallbackiem do `products_data`. + +## Gdzie to jest wykorzystywane + +- Pipeline produktowy: + - `/cron/cron_products` + - etap `fetch` -> `products_history`, + - etap agregacji -> `products_history_30` + `products_aggregate`, + - etap finalny -> `products_temp` budowane z `products_aggregate`. +- Widok tabeli produktow `/products`: + - dane nadal czytane z `products_temp`, ale `products_temp` jest teraz zasilane agregatami z `products_aggregate`. + - Dodany helper `sync_campaigns_snapshot_for_client()` dla nowego przebiegu kampanii. + - Dodany helper `sync_campaign_terms_backfill_for_client()` dla kroku fraz (history + agregacja). + - Tryb wykonania nowego pipeline kampanii: 1 dzien = 1 wywolanie CRON. + - Na jednym wywolaniu: kampanie + ad groups + search terms history + agregacja search terms dla jednego dnia. + - Kolejne wywolanie przechodzi do kolejnego dnia dla tego samego klienta. + - Tryb debug dla nowego CRON: + - `?debug=true` zwraca czytelny HTML (podsumowanie + pelny payload), + - bez debug zwracany jest standardowy JSON. + - Dodany helper `cleanup_pipeline_rows_outside_window()` aby pipeline kampanii trzymal tylko aktualne okno dat. + - Filtry klientow w nowym CRON kampanii sa odporne na stare dane (`NULL`): `COALESCE(active,0)`, `COALESCE(deleted,0)`, `TRIM(COALESCE(google_ads_customer_id,''))`. + - Dodana kompatybilnosc schematu `clients` bez kolumny `deleted`: + - helpery: `clients_has_column()`, `sql_clients_not_deleted()`, `sql_clients_deleted()`, + - nowy pipeline kampanii (`cron_campaigns`/`cron_universal`) nie wywala sie na bazie bez `deleted`. + - `get_conversion_window_days( $prefer_config = false )` uwzglednia teraz konfiguracje z `config.php`. + - `sync_campaign_ad_groups_for_client()` dostal parametr `as_of_date`. + +- `autoload/services/class.GoogleAdsApi.php` + - `get_ad_groups_30_days()` wspiera teraz parametr `as_of_date` i zakres dat `[as_of_date-29, as_of_date]`. + - `get_ad_groups_all_time()` wspiera teraz parametr `as_of_date` (filtr `segments.date <= as_of_date` z fallbackiem). + +## Gdzie to jest wykorzystywane + +- Głowny CRON kampanii: `/cron/cron_campaigns` -> `\controls\Cron::cron_campaigns()`. +- Uniwersalny CRON pipeline (zalecany endpoint): `/cron/cron_universal` -> `\controls\Cron::cron_universal()` (aktualnie deleguje do kroku kampanii). +- Archiwalny CRON kampanii (stara logika): `/cron/cron_campaigns_archive`. +- Dane do wykresow/tabel kampanii pozostaja pobierane z `campaigns_history`. + +# 2026-02-20 - CRON uniwersalny jako glowny endpoint (1 dzien na wywolanie) + +## Zmienione pliki + +- `autoload/controls/class.Cron.php` + - `cron_universal()` nie deleguje juz do `cron_campaigns()`. + - W jednym wywolaniu realizuje sekwencje: + - `kampanie` (snapshot + ad groups + search terms + agregacja), + - `produkty` (fetch + `products_history_30` + `products_aggregate` + `products_temp`). + - Tryb pracy pozostaje: `1 wywolanie = 1 klient + 1 dzien`. + - Status dnia jest zapisywany do `cron_sync_status` dla obu pipeline: + - `campaigns`, + - `products`. + - Gdy krok kampanii zwroci blad, krok produktow dla tego dnia jest pomijany (`products_sync_skipped_reason=campaigns_failed`). + +## Gdzie to jest wykorzystywane + +- Docelowy adres CRON: + - `/cron/cron_universal?debug=true` +- Stare endpointy (`/cron/cron_campaigns`, `/cron/cron_products`) pozostaja w kodzie, ale nie sa docelowa sciezka wykonywania. + +# 2026-02-20 - Poprawka niezaleznosci pipeline w `cron_universal` + +## Problem + +- `campaigns` mialo juz 100% (`done`) i `cron_universal` konczyl wykonanie, mimo ze `products` mial jeszcze zalegle daty. + +## Zmienione pliki + +- `autoload/controls/class.Cron.php` + - `cron_universal()` wybiera teraz aktywnego klienta niezaleznie dla obu pipeline: + - `campaigns`, + - `products`. + - Zakonczenie "wszyscy przetworzeni" następuje dopiero, gdy **oba** pipeline nie maja juz aktywnych pozycji. + - Dodane osobne liczenie pozostalych dat: + - `campaigns_remaining_dates`, + - `products_remaining_dates`. + - Statusy `done/pending` sa zapisywane osobno dla kazdego pipeline; produkty nie sa juz blokowane przez sam fakt, ze kampanie sa skonczone globalnie. + - Ujednolicenie trybu `client_id`: + - kampanie i produkty wykonują sie niezaleznie (w tym samym wywolaniu), a bledy sa laczone tylko w odpowiedzi. + +# 2026-02-20 - Usuniecie `products_data` + +## Zmienione pliki + +- `migrations/017_drop_products_data.sql` + - Dodana migracja usuwajaca tabele `products_data`. + +- `autoload/factory/class.Products.php` + - `get_product_data()` czyta dane tylko z `products`. + - `set_product_data()` zapisuje dane tylko do `products`. + +- `autoload/controls/class.Cron.php` + - diagnostyka URL i wybieranie produktow bez URL opiera sie juz tylko o `products.product_url`. + +- `docs/database.sql` + - usunieta definicja tabeli `products_data`. + +- `migrations/demo_data.sql` + - usuniete operacje `INSERT/DELETE` na `products_data`, + - etykiety demo (`custom_label_4`) sa ustawiane bezposrednio w `products`. + +## Gdzie to jest wykorzystywane + +- Dane produktowe (`title`, `description`, `google_product_category`, `custom_label_3`, `custom_label_4`, `product_url`) sa trzymane tylko w `products`. + +# 2026-02-20 - Ostatni krok `cron_universal`: URL z Merchant + alerty brakow + +## Zmienione pliki + +- `autoload/controls/class.Cron.php` + - Dodany helper `sync_products_urls_and_alerts_for_client()`. + - Na koncu przebiegu `cron_universal` (zarowno tryb automatyczny, jak i `client_id`) wykonywany jest krok: + - pobranie URL produktow z Google Merchant Center dla produktow bez URL, + - zapis URL do `products.product_url`. + - Gdy `offer_id` nie istnieje w Merchant Center, tworzony/aktualizowany jest alert w `campaign_alerts`: + - `alert_type = products_missing_in_merchant_center`, + - scope techniczny: `campaign_external_id = 0`, `ad_group_external_id = 0`, + - `meta_json` zawiera m.in. listy `missing_offer_ids` i `missing_product_ids`. + - Gdy w danym dniu brak brakujacych produktow, dzienny alert tego typu jest czyszczony. + - Do odpowiedzi cron dodane pola diagnostyczne: + - `merchant_urls_checked`, + - `merchant_urls_updated`, + - `merchant_missing_in_mc_count`, + - `merchant_missing_offer_ids`. + - `cron_universal` ma dodatkowy fallback niezalezny od pipeline `campaigns/products`: + - gdy oba pipeline sa zakonczone, ale sa jeszcze produkty bez URL, uruchamia sam krok Merchant URL + alerty (`merchant_only=1`), + - dopiero brak takich produktow daje komunikat "Wszyscy aktywni klienci zostali przetworzeni...". + - Krok Merchant URL nie jest wykonywany dla kazdego dnia okna; dziala jako osobny etap po zakonczeniu `campaigns/products`. + - Do zapytan do GMC trafiaja tylko produkty z `products.product_url IS NULL` i `merchant_url_not_found = 0`. + - Na jedno wywolanie wykonywana jest jedna paczka sprawdzen (limit z `config.php`: `cron_products_urls_limit_per_client`, ustawiony na `100`). + - Produkty, ktorych GMC nie zwraca (brak URL), sa oznaczane: + - `products.merchant_url_not_found = 1`, + - `products.merchant_url_last_check = NOW()`, + - dzieki temu nie sa wysylane ponownie w nieskonczonosc. + - Alert `products_missing_in_merchant_center` jest liczony na podstawie calej aktualnej puli `merchant_url_not_found = 1` (nie tylko bieżącej paczki), wiec nie znika przy `checked_products = 0`. + - Alerty sa per produkt (1 alert = 1 produkt): + - dla kazdego produktu bez URL i z `merchant_url_not_found = 1` tworzony jest osobny wpis w `campaign_alerts`, + - tresc alertu zawiera nazwe produktu (fallback: `name`, dalej `offer_id`) i `offer_id`, + - technicznie: `campaign_external_id = products.id`, co stabilizuje unikalnosc wpisu. + +- `migrations/018_products_merchant_url_flags.sql` + - Dodane kolumny w `products`: + - `merchant_url_not_found` (TINYINT, domyslnie 0), + - `merchant_url_last_check` (DATETIME). + - Normalizacja: puste/sztuczne `product_url` (`'', '0', '-', 'null'`) ustawiane na `NULL`. + +- `autoload/factory/class.Products.php` + - Przy zapisie `product_url`: + - ustawiany jest `merchant_url_last_check`, + - dla poprawnego URL resetowane jest `merchant_url_not_found = 0`. + +# 2026-02-20 - Alerty na stronie `/products` dla klient + kampania + +## Zmienione pliki + +- `autoload/factory/class.Products.php` + - `get_scope_alerts()` nie wymaga juz wybranej grupy reklam: + - minimalny scope: `client_id + campaign_id`, + - filtr `ad_group_id` jest stosowany tylko opcjonalnie (gdy grupa jest wybrana). + +- `templates/products/main_view.php` + - `load_scope_alerts()` pobiera alerty juz dla kombinacji `klient + kampania`. + - Sekcja alertow ma zaktualizowany opis: kampania + opcjonalna grupa reklam. + +## Gdzie to jest wykorzystywane + +- `/products` + - Panel alertow pod filtrami pokazuje alerty: + - dla calej kampanii (gdy grupa reklam nie jest wybrana), + - lub zawezone do konkretnej grupy (gdy grupa reklam jest wybrana). + +# 2026-02-20 - Etykietowanie alertow Merchant (bez falszywej kampanii) + +## Zmienione pliki + +- `autoload/controls/class.Cron.php` + - Dla alertu `products_missing_in_merchant_center` nie jest juz zapisywany `product_id` w `campaign_external_id`. + - Pola scope kampanii/grupy sa zapisywane jako `0` (alert produktowy, bez przypisania do kampanii). + +- `templates/campaign_alerts/main_view.php` + - Dla alertu `products_missing_in_merchant_center` tabela alertow pokazuje: + - Kampania: `Produkt (Merchant Center)`, + - Grupa reklam: `---`. + - Dla pozostalych alertow fallback `Kampania #...` / `Grupa reklam #...` dziala tylko dla dodatnich external_id; dla `0` pokazuje neutralne etykiety. + +# 2026-02-20 - Powiazanie `campaign_alerts` z `products` + +## Zmienione pliki + +- `migrations/019_campaign_alerts_product_id.sql` + - Dodana kolumna `campaign_alerts.product_id` (NULL) oraz indeks `idx_alert_product`. + +- `autoload/controls/class.Cron.php` + - Alerty `products_missing_in_merchant_center` zapisuja `product_id` w tabeli `campaign_alerts`. + - Dla zachowania unikalnosci dziennej per produkt, techniczny `campaign_external_id` pozostaje rowny `product_id`. + +- `autoload/factory/class.CampaignAlerts.php` + - `get_alerts()` zwraca teraz rowniez pole `product_id`. + +- `docs/database.sql` + - Dodana aktualna definicja tabeli `campaign_alerts` z kolumna `product_id`. + +# 2026-02-20 - CRON produktow: `title` nie jest uzupelniany automatycznie + +## Zmienione pliki + +- `autoload/controls/class.Cron.php` + - W syncu produktow do tabeli `products` CRON nie zapisuje juz pola `title`. + - Dla nowych produktow CRON zapisuje tylko `name` (bez `title`). + - Dla istniejacych produktow usunieto automatyczne uzupelnianie pustego `title`. + +## Gdzie to jest wykorzystywane + +- `/cron/cron_universal` + - automatyczny import produktow nie nadpisuje ani nie uzupelnia `products.title`, + - `title` pozostaje polem do recznej edycji i wysylki do GMC. + +# 2026-02-20 - Lista produktow z 0 wyswietlen (30 dni) na `/products` + +## Zmienione pliki + +- `autoload/factory/class.Products.php` + - Dodana metoda `get_products_without_impressions_30( $client_id, $campaign_id, $limit )`. + - Zwraca produkty z wybranej kampanii, ktore maja sume `impressions_30 = 0` na podstawie `products_aggregate`. + - Dodatkowy filtr `ad_group_id` (opcjonalny), aby lista byla zgodna z aktualnym filtrem grupy reklam na widoku. + +- `autoload/controls/class.Products.php` + - Dodany endpoint `get_products_without_impressions_30()`. + - Zwraca JSON: `status`, `products[]`, `count` i przyjmuje opcjonalnie `ad_group_id`. + +- `templates/products/main_view.php` + - Dodana sekcja nad tabela produktow: + - "Produkty do sprawdzenia (0 wyswietlen w ostatnich 30 dniach)". + - Sekcja pojawia sie dla wybranego `klient + kampania`. + - Lista odswieza sie przy zmianie klienta/kampanii/grupy oraz po zaladowaniu strony. + +## Gdzie to jest wykorzystywane + +- `/products` + - pomocnicza lista produktow potencjalnie nieistniejacych / wymagajacych weryfikacji (0 wyswietlen w 30 dni dla wybranej kampanii). + +# 2026-02-20 - Ustawienia CRON: poprawka licznika klientow + usuniecie "Krok 1/Krok 2" + +## Zmienione pliki + +- `autoload/controls/class.Users.php` + - Licznik `Klienci z Google Ads ID` liczy teraz klientow z: + - `COALESCE(active, 0) = 1`, + - `TRIM(COALESCE(google_ads_customer_id, '')) <> ''`. + - Analogicznie poprawione filtry dla klientow Merchant i zapytan pomocniczych (wg `active`). + - Harmonogram krokow (`Krok 1`, `Krok 2`) w danych dashboardu CRON jest pusty. + +- `templates/users/settings.php` + - Usunieta sekcja wizualna harmonogramu krokow CRON (`Krok 1` / `Krok 2`). + - Usunieta obsluga renderowania tej sekcji w JS odswiezajacym status CRON. + +## Gdzie to jest wykorzystywane + +- `/settings?settings_tab=cron` + - licznik klientow z Google Ads ID pokazuje poprawna wartosc na podstawie aktywnych klientow (`active = 1`), + - brak sekcji "Krok 1 / Krok 2". + +# 2026-02-20 - `/products` czyta bezposrednio z `products_aggregate` + +## Zmienione pliki + +- `autoload/factory/class.Products.php` + - Zapytania dla listy produktow i licznikow zostaly przepiete z `products_temp` na `products_aggregate`: + - `get_products()`, + - `get_roas_bounds()`, + - `get_records_total_products()`, + - `get_product_full_context()`. + - Metryki all-time sa liczone z pol: + - `impressions_all_time`, `clicks_all_time`, `cost_all_time`, `conversions_all_time`, `conversion_value_all_time`. + - Metryki 30d sa czytane z: + - `impressions_30`, `clicks_30`. + +## Gdzie to jest wykorzystywane + +- `/products` + - tabela i liczniki nie zaleza juz od `products_temp`; biora dane bezposrednio z `products_aggregate`. + +# 2026-02-20 - `custom_label_4` tylko z tabeli `products` + +## Ustalenie + +- Etykieta `custom_label_4` jest czytana i zapisywana z tabeli `products`. +- Agregaty (`products_aggregate`) nie sa zrodlem dla pola `custom_label_4`. diff --git a/layout/style.css b/layout/style.css index 9e917cb..fbee538 100644 --- a/layout/style.css +++ b/layout/style.css @@ -1 +1 @@ -*{box-sizing:border-box}body{font-family:"Roboto",sans-serif;margin:0;padding:0;font-size:14px;color:#4e5e6a;background:#f4f6f9;max-width:100vw;overflow-x:hidden}.hide{display:none}small{font-size:.75em}.text-right{text-align:right}.text-bold{font-weight:700 !important}.nowrap{white-space:nowrap}body.unlogged{background:#f4f6f9;margin:0;padding:0}.login-container{display:flex;min-height:100vh}.login-brand{flex:0 0 45%;background:linear-gradient(135deg, #1E2A3A 0%, #2C3E57 50%, #6690F4 100%);display:flex;align-items:center;justify-content:center;padding:60px;position:relative;overflow:hidden}.login-brand::before{content:"";position:absolute;top:-50%;right:-50%;width:100%;height:100%;background:radial-gradient(circle, rgba(102, 144, 244, 0.15) 0%, transparent 70%);border-radius:50%}.login-brand .brand-content{position:relative;z-index:1;color:#fff;max-width:400px}.login-brand .brand-logo{font-size:48px;font-weight:300;margin-bottom:20px;letter-spacing:-1px}.login-brand .brand-logo strong{font-weight:700}.login-brand .brand-tagline{font-size:18px;opacity:.85;line-height:1.6;margin-bottom:50px}.login-brand .brand-features .feature{display:flex;align-items:center;gap:15px;margin-bottom:20px;opacity:.8}.login-brand .brand-features .feature i{font-size:20px;width:40px;height:40px;display:flex;align-items:center;justify-content:center;background:hsla(0,0%,100%,.1);border-radius:10px}.login-brand .brand-features .feature span{font-size:15px}.login-form-wrapper{flex:1;display:flex;align-items:center;justify-content:center;padding:60px;background:#fff}.login-box{width:100%;max-width:420px}.login-box .login-header{margin-bottom:35px}.login-box .login-header h1{font-size:28px;font-weight:700;color:#2d3748;margin:0 0 8px}.login-box .login-header p{color:#718096;font-size:15px;margin:0}.login-box .form-group{margin-bottom:20px}.login-box .form-group label{display:block;font-size:13px;font-weight:600;color:#2d3748;margin-bottom:6px}.login-box .input-with-icon{position:relative}.login-box .input-with-icon i{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#a0aec0;font-size:14px}.login-box .input-with-icon .form-control{padding-left:42px}.login-box .form-control{width:100%;height:46px;border:2px solid #e2e8f0;border-radius:8px;padding:0 14px;font-size:14px;font-family:"Roboto",sans-serif;color:#2d3748;transition:border-color .3s,box-shadow .3s}.login-box .form-control::-moz-placeholder{color:#cbd5e0}.login-box .form-control::placeholder{color:#cbd5e0}.login-box .form-control:focus{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.15);outline:none}.login-box .form-error{color:#c00;font-size:12px;margin-top:4px}.login-box .checkbox-group .checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:#718096;font-weight:400}.login-box .checkbox-group .checkbox-label input[type=checkbox]{width:16px;height:16px;accent-color:#6690f4}.login-box .btn-login{width:100%;height:48px;font-size:15px;font-weight:600;border-radius:8px;display:flex;align-items:center;justify-content:center;gap:8px}.login-box .btn-login.disabled{opacity:.7;pointer-events:none}.login-box .alert{display:none;padding:12px 16px;border-radius:8px;font-size:13px;margin-bottom:20px}.login-box .alert.alert-danger{background:#fff5f5;color:#c00;border:1px solid #fed7d7}.login-box .alert.alert-success{background:#f0fff4;color:#276749;border:1px solid #c6f6d5}@media(max-width: 768px){.login-brand{display:none}.login-form-wrapper{padding:30px 20px}}body.logged{display:flex;min-height:100vh;background:#f4f6f9}.sidebar{width:260px;min-height:100vh;background:#1e2a3a;position:fixed;top:0;left:0;z-index:1000;display:flex;flex-direction:column;transition:width .3s ease;overflow:hidden}.sidebar.collapsed{width:70px}.sidebar.collapsed .sidebar-header{padding:16px 0;justify-content:center}.sidebar.collapsed .sidebar-header .sidebar-logo{display:none}.sidebar.collapsed .sidebar-header .sidebar-toggle i{transform:rotate(180deg)}.sidebar.collapsed .sidebar-nav ul li a{padding:12px 0;justify-content:center}.sidebar.collapsed .sidebar-nav ul li a span{display:none}.sidebar.collapsed .sidebar-nav ul li a i{margin-right:0;font-size:18px}.sidebar.collapsed .sidebar-nav ul li.nav-group .nav-group-label{padding:12px 0;justify-content:center}.sidebar.collapsed .sidebar-nav ul li.nav-group .nav-group-label span{display:none}.sidebar.collapsed .sidebar-nav ul li.nav-group .nav-group-label i{margin-right:0;font-size:18px}.sidebar.collapsed .sidebar-footer .sidebar-user{justify-content:center}.sidebar.collapsed .sidebar-footer .sidebar-user .user-info{display:none}.sidebar.collapsed .sidebar-footer .sidebar-logout{justify-content:center}.sidebar.collapsed .sidebar-footer .sidebar-logout span{display:none}.sidebar.collapsed .nav-divider{margin:8px 15px}.sidebar-header{display:flex;align-items:center;justify-content:space-between;padding:20px 20px 16px;border-bottom:1px solid hsla(0,0%,100%,.08)}.sidebar-header .sidebar-logo a{color:#fff;text-decoration:none;font-size:24px;font-weight:300;letter-spacing:-0.5px}.sidebar-header .sidebar-logo a strong{font-weight:700}.sidebar-header .sidebar-toggle{background:none;border:none;color:#a8b7c7;cursor:pointer;padding:6px;border-radius:6px;transition:all .3s}.sidebar-header .sidebar-toggle:hover{background:hsla(0,0%,100%,.08);color:#fff}.sidebar-header .sidebar-toggle i{transition:transform .3s}.sidebar-nav{flex:1;padding:12px 0;overflow-y:auto}.sidebar-nav ul{list-style:none;margin:0;padding:0}.sidebar-nav ul li.nav-group{margin-bottom:4px}.sidebar-nav ul li.nav-group .nav-group-label{display:flex;align-items:center;padding:11px 20px;color:#d5deea;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;border-left:3px solid rgba(0,0,0,0)}.sidebar-nav ul li.nav-group .nav-group-label i{width:20px;text-align:center;margin-right:12px;font-size:14px;color:#b6c4d3}.sidebar-nav ul li.nav-group .nav-submenu{margin:0;padding:0;list-style:none}.sidebar-nav ul li.nav-group .nav-submenu li a{padding-left:44px}.sidebar-nav ul li.nav-group.active>.nav-group-label{color:#fff;background:rgba(102,144,244,.12);border-left-color:#6690f4}.sidebar-nav ul li.nav-group.active>.nav-group-label i{color:#6690f4}.sidebar-nav ul li.nav-divider{height:1px;background:hsla(0,0%,100%,.08);margin:8px 20px}.sidebar-nav ul li a{display:flex;align-items:center;padding:11px 20px;color:#a8b7c7;text-decoration:none;font-size:14px;transition:all .2s;border-left:3px solid rgba(0,0,0,0)}.sidebar-nav ul li a i{width:20px;text-align:center;margin-right:12px;font-size:15px}.sidebar-nav ul li a:hover{background:#263548;color:#fff}.sidebar-nav ul li.active>a{background:rgba(102,144,244,.15);color:#fff;border-left-color:#6690f4}.sidebar-nav ul li.active>a i{color:#6690f4}.badge-alerts-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;margin-left:8px;border-radius:50%;font-size:11px;font-weight:600;line-height:1;background:#fff;color:#6690f4}.sidebar-footer{padding:16px 20px;border-top:1px solid hsla(0,0%,100%,.08)}.sidebar-footer .sidebar-user{display:flex;align-items:center;gap:10px;margin-bottom:12px}.sidebar-footer .sidebar-user .user-avatar{width:34px;height:34px;border-radius:50%;background:rgba(102,144,244,.2);display:flex;align-items:center;justify-content:center;color:#6690f4;font-size:14px;flex-shrink:0}.sidebar-footer .sidebar-user .user-info{overflow:hidden}.sidebar-footer .sidebar-user .user-info .user-email{color:#a8b7c7;font-size:12px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sidebar-footer .sidebar-logout{display:flex;align-items:center;gap:8px;color:#e53e3e;text-decoration:none;font-size:13px;padding:8px 10px;border-radius:6px;transition:all .2s}.sidebar-footer .sidebar-logout i{font-size:14px}.sidebar-footer .sidebar-logout:hover{background:rgba(229,62,62,.1)}.main-wrapper{margin-left:260px;flex:1;min-height:100vh;transition:margin-left .3s ease;display:flex;flex-direction:column}.main-wrapper.expanded{margin-left:70px}.topbar{height:56px;background:#fff;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;padding:0 25px;position:sticky;top:0;z-index:500}.topbar .topbar-toggle{background:none;border:none;color:#4e5e6a;cursor:pointer;padding:8px 10px;border-radius:6px;font-size:16px;margin-right:15px;transition:all .2s}.topbar .topbar-toggle:hover{background:#f4f6f9}.topbar .topbar-breadcrumb{font-size:16px;font-weight:600;color:#2d3748}.content{flex:1;padding:25px}.app-alert{background:#ebf8ff;border:1px solid #bee3f8;color:#2b6cb0;padding:12px 16px;border-radius:8px;margin-bottom:20px;font-size:14px}.btn{padding:10px 20px;transition:all .2s ease;color:#fff;border:0;border-radius:6px;cursor:pointer;display:inline-flex;text-decoration:none;gap:6px;justify-content:center;align-items:center;font-size:14px;font-family:"Roboto",sans-serif;font-weight:500}.btn.btn_small,.btn.btn-xs,.btn.btn-sm{padding:5px 10px;font-size:12px}.btn.btn_small i,.btn.btn-xs i,.btn.btn-sm i{font-size:11px}.btn.btn-success{background:#57b951}.btn.btn-success:hover{background:#4a9c3b}.btn.btn-primary{background:#6690f4}.btn.btn-primary:hover{background:#3164db}.btn.btn-danger{background:#c00}.btn.btn-danger:hover{background:#b30000}.btn.disabled{opacity:.6;pointer-events:none}.form-control{border:1px solid #e2e8f0;border-radius:6px;height:38px;width:100%;padding:6px 12px;font-family:"Roboto",sans-serif;font-size:14px;color:#2d3748;transition:border-color .2s,box-shadow .2s}.form-control option{padding:5px}.form-control:focus{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1);outline:none}input[type=checkbox]{border:1px solid #e2e8f0}table{border-collapse:collapse;font-size:13px}.table{width:100%}.table th,.table td{border:1px solid #e2e8f0;padding:8px 10px}.table th{background:#f7fafc;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.03em;color:#718096}.table td.center{text-align:center}.table td.left{text-align:left}.table.table-sm td{padding:5px !important}.table input.form-control{font-size:13px;height:32px}.card{background:#fff;padding:20px;border-radius:8px;color:#2d3748;font-size:14px;box-shadow:0 1px 3px rgba(0,0,0,.06)}.card.mb25{margin-bottom:20px}.card .card-header{font-weight:600;font-size:15px}.card .card-body{padding-top:12px}.card .card-body table th,.card .card-body table td{font-size:13px}.card .card-body table th.bold,.card .card-body table td.bold{font-weight:600}.card .card-body table th.text-right,.card .card-body table td.text-right{text-align:right}.card .card-body table th.text-center,.card .card-body table td.text-center{text-align:center}.action_menu{display:flex;margin-bottom:20px;gap:12px}.action_menu .btn{padding:8px 16px}.action_menu .btn.btn_add{background:#57b951}.action_menu .btn.btn_add:hover{background:#4a9c3b}.action_menu .btn.btn_cancel{background:#c00}.action_menu .btn.btn_cancel:hover{background:#b30000}.settings-tabs{display:flex;gap:8px;margin-bottom:18px}.settings-tabs .settings-tab{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:8px;text-decoration:none;color:#6b7a89;background:#e9eef5;border:1px solid #d8e0ea;font-size:13px;font-weight:600;transition:all .2s}.settings-tabs .settings-tab:hover{color:#2d3748;background:#dde6f2}.settings-tabs .settings-tab.active{color:#fff;background:#6690f4;border-color:#6690f4}.settings-card{background:#fff;border-radius:10px;padding:28px;box-shadow:0 1px 4px rgba(0,0,0,.06)}.settings-card .settings-card-header{display:flex;align-items:center;gap:14px;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid #e2e8f0}.settings-card .settings-card-header .settings-card-icon{width:44px;height:44px;border-radius:10px;background:rgb(225.706097561,233.7475609756,252.893902439);color:#6690f4;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}.settings-card .settings-card-header h3{margin:0;font-size:17px;font-weight:600;color:#2d3748}.settings-card .settings-card-header small{color:#8899a6;font-size:13px}.settings-card .settings-field{margin-bottom:18px}.settings-card .settings-field label{display:block;font-size:13px;font-weight:600;color:#2d3748;margin-bottom:6px}.settings-card .settings-input-wrap{position:relative}.settings-card .settings-input-wrap .settings-input-icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#a0aec0;font-size:14px;pointer-events:none}.settings-card .settings-input-wrap .form-control{padding-left:38px}.settings-card .settings-input-wrap .settings-toggle-pw{position:absolute;right:4px;top:50%;transform:translateY(-50%);background:none;border:none;color:#a0aec0;cursor:pointer;padding:6px 10px;font-size:14px;transition:color .2s}.settings-card .settings-input-wrap .settings-toggle-pw:hover{color:#6690f4}.settings-card .settings-field .settings-toggle-label{display:inline-flex;align-items:center;gap:10px;cursor:pointer;font-size:14px;font-weight:500;-webkit-user-select:none;-moz-user-select:none;user-select:none;margin-bottom:0;width:100%}.settings-card .settings-field .settings-toggle-label .settings-toggle-text{flex:1 1 auto;min-width:0;line-height:1.35}.settings-card .settings-toggle-checkbox{display:none}.settings-card .settings-toggle-checkbox+.settings-toggle-switch{display:inline-block;position:relative;width:44px;height:24px;background:#ccc;border-radius:12px;transition:background .2s;flex-shrink:0}.settings-card .settings-toggle-checkbox+.settings-toggle-switch::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;background:#fff;border-radius:50%;transition:transform .2s}.settings-card .settings-toggle-checkbox:checked+.settings-toggle-switch{background:#22c55e}.settings-card .settings-toggle-checkbox:checked+.settings-toggle-switch::after{transform:translateX(20px)}.settings-card .settings-fields-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 24px}@media(max-width: 768px){.settings-card .settings-fields-grid{grid-template-columns:1fr}}.settings-card .settings-alert-error{display:flex;align-items:center;gap:10px;background:#fff5f5;color:#c00;border:1px solid #fed7d7;border-radius:8px;padding:12px 16px;margin-bottom:20px;font-size:13px}.settings-card .settings-alert-error i{font-size:16px;flex-shrink:0}.clients-page .clients-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.clients-page .clients-header h2{margin:0;font-size:20px;font-weight:600;color:#2d3748}.clients-page .clients-header h2 i{color:#6690f4;margin-right:8px}.clients-page .clients-table-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow:hidden}.clients-page .clients-table-wrap .table{margin:0}.clients-page .clients-table-wrap .table thead th{background:#f8fafc;border-bottom:2px solid #e2e8f0;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;padding:14px 20px}.clients-page .clients-table-wrap .table tbody td{padding:14px 20px;vertical-align:middle;border-bottom:1px solid #f1f5f9}.clients-page .clients-table-wrap .table tbody tr:hover{background:#f8fafc}.clients-page .clients-table-wrap .table .client-id{color:#8899a6;font-size:13px;font-weight:600}.clients-page .clients-table-wrap .table .client-name{font-weight:600;color:#2d3748}.clients-page .badge-id{display:inline-block;background:#eef2ff;color:#6690f4;font-size:13px;font-weight:600;padding:4px 10px;border-radius:6px;font-family:monospace}.clients-page .actions-cell{text-align:center;white-space:nowrap}.clients-page .btn-icon{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:8px;border:none;cursor:pointer;font-size:14px;transition:all .2s;margin:0 2px}.clients-page .btn-icon.btn-icon-edit{background:#eef2ff;color:#6690f4}.clients-page .btn-icon.btn-icon-edit:hover{background:#6690f4;color:#fff}.clients-page .btn-icon.btn-icon-delete{background:#fff5f5;color:#c00}.clients-page .btn-icon.btn-icon-delete:hover{background:#c00;color:#fff}.clients-page .btn-icon.btn-icon-sync{background:#f0fdf4;color:#16a34a}.clients-page .btn-icon.btn-icon-sync:hover{background:#16a34a;color:#fff}.clients-page .btn-icon.btn-icon-sync:disabled{opacity:.7;cursor:wait}.clients-page .btn-icon.btn-icon-sync.is-queued{background:#fef3c7;color:#d97706}.clients-page .client-sync-bars{display:flex;flex-direction:column;gap:4px}.clients-page .client-sync-row{display:flex;align-items:center;gap:4px}.clients-page .client-sync-label{font-size:11px;font-weight:600;color:#8899a6;width:18px;flex-shrink:0}.clients-page .client-sync-track{flex:1;height:6px;border-radius:999px;background:#e9eef5;overflow:hidden}.clients-page .client-sync-fill{height:100%;border-radius:999px;background:#cbd5e0;transition:width .4s ease}.clients-page .client-sync-fill.is-active{background:linear-gradient(90deg, #5A9BFF 0%, #2E6BDF 100%)}.clients-page .client-sync-fill.is-done{background:#57b951}.clients-page .client-sync-pct{font-size:11px;font-weight:600;color:#8899a6;width:32px;text-align:right;flex-shrink:0}.clients-page .empty-state{text-align:center;padding:50px 20px !important;color:#a0aec0}.clients-page .empty-state i{font-size:40px;margin-bottom:12px;display:block}.clients-page .empty-state p{margin:0;font-size:15px}.btn-secondary{background:#e2e8f0;color:#2d3748;border:none;padding:8px 18px;border-radius:6px;font-size:14px;cursor:pointer;transition:background .2s}.btn-secondary:hover{background:#cbd5e0}.campaigns-page{max-width:100%;overflow-x:hidden;width:100%}.campaigns-page .campaigns-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.campaigns-page .campaigns-header h2{margin:0;font-size:20px;font-weight:600;color:#2d3748}.campaigns-page .campaigns-header h2 i{color:#6690f4;margin-right:8px}.campaigns-page .campaigns-filters{display:flex;flex-wrap:wrap;gap:20px;margin-bottom:20px}.campaigns-page .campaigns-filters .filter-group{flex:1;min-width:0}.campaigns-page .campaigns-filters .filter-group label{display:block;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;margin-bottom:6px}.campaigns-page .campaigns-filters .filter-group label i{margin-right:4px}.campaigns-page .campaigns-filters .filter-group .form-control{width:100%;padding:10px 14px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;color:#2d3748;background:#fff;transition:border-color .2s;-moz-appearance:none;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}.campaigns-page .campaigns-filters .filter-group .form-control:focus{outline:none;border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.campaigns-page .campaigns-filters .filter-group .filter-with-action{display:flex;align-items:center;gap:8px}.campaigns-page .campaigns-filters .filter-group .filter-with-action .form-control{flex:1}.campaigns-page .campaigns-filters .filter-group .filter-with-action .btn-icon{flex-shrink:0;width:42px;height:42px;display:inline-flex;align-items:center;justify-content:center;border-radius:8px;border:none;cursor:pointer;font-size:14px;transition:all .2s}.campaigns-page .campaigns-filters .filter-group .filter-with-action .btn-icon.btn-icon-delete{background:#fff5f5;color:#c00}.campaigns-page .campaigns-filters .filter-group .filter-with-action .btn-icon.btn-icon-delete:hover{background:#c00;color:#fff}.campaigns-page .campaigns-filters .filter-group-campaign-multi{flex:2 !important}.campaigns-page .campaigns-filters .campaign-dropdown{flex:1;min-width:0;position:relative}.campaigns-page .campaigns-filters .campaign-dropdown-trigger{width:100%;padding:10px 14px;padding-right:32px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;color:#2d3748;background:#fff;cursor:pointer;display:flex;align-items:center;transition:border-color .2s;position:relative;min-height:42px;box-sizing:border-box}.campaigns-page .campaigns-filters .campaign-dropdown-trigger .campaign-dropdown-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.campaigns-page .campaigns-filters .campaign-dropdown-trigger .campaign-dropdown-text.is-placeholder{color:#8899a6}.campaigns-page .campaigns-filters .campaign-dropdown-trigger .campaign-dropdown-arrow{position:absolute;right:12px;font-size:10px;color:#8899a6;transition:transform .2s}.campaigns-page .campaigns-filters .campaign-dropdown.is-open .campaign-dropdown-trigger{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.campaigns-page .campaigns-filters .campaign-dropdown.is-open .campaign-dropdown-arrow{transform:rotate(180deg)}.campaigns-page .campaigns-filters .campaign-dropdown.is-open .campaign-dropdown-menu{display:block}.campaigns-page .campaigns-filters .campaign-dropdown-menu{display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:100;max-height:280px;overflow-y:auto;background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.1);padding:4px 0}.campaigns-page .campaigns-filters .campaign-dropdown-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:14px;color:#2d3748;margin:0;transition:background .15s}.campaigns-page .campaigns-filters .campaign-dropdown-item:hover{background:#f8fafc}.campaigns-page .campaigns-filters .campaign-dropdown-item.is-checked{background:#eef2ff}.campaigns-page .campaigns-filters .campaign-dropdown-item input[type=checkbox]{width:16px;height:16px;cursor:pointer;flex-shrink:0;accent-color:#6690f4}.campaigns-page .campaigns-filters .campaign-dropdown-item span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.campaigns-page .campaigns-list-panel{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);margin-bottom:20px;overflow:hidden}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #e2e8f0;gap:12px}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left{display:flex;align-items:center;gap:8px;font-size:13px;color:#4e5e6a}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left input[type=checkbox]{width:16px;height:16px;cursor:pointer}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left label{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;margin:0}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left .campaigns-selected-count{margin-left:12px;color:#8899a6}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-right .campaigns-bulk-delete-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;background:#fff5f5;color:#c00;transition:all .2s}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-right .campaigns-bulk-delete-btn:hover:not(:disabled){background:#c00;color:#fff}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-right .campaigns-bulk-delete-btn:disabled{opacity:.4;cursor:not-allowed}.campaigns-page .campaigns-list-panel .campaigns-list-items{display:flex;flex-wrap:wrap;gap:0;padding:8px 8px;max-height:220px;overflow-y:auto}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item{display:flex;align-items:center;gap:8px;padding:6px 12px;margin:2px;border-radius:6px;font-size:13px;color:#2d3748;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background .15s}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item:hover{background:#f0f4ff}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item input[type=checkbox]{width:15px;height:15px;cursor:pointer;flex-shrink:0}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item .campaigns-list-item-name{white-space:nowrap}.campaigns-page .campaigns-chart-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);padding:20px;margin-bottom:20px;min-height:350px;overflow:hidden}.campaigns-page .campaigns-chart-wrap #container{max-width:100%}.campaigns-page .campaigns-table-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow-x:auto;-ms-overflow-style:none;scrollbar-width:none;max-width:100%}.campaigns-page .campaigns-table-wrap::-webkit-scrollbar{display:none}.campaigns-page .campaigns-table-wrap .table{margin:0;width:100% !important}.campaigns-page .campaigns-table-wrap .table thead th{background:#f8fafc;border-bottom:2px solid #e2e8f0;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;padding:12px 16px;white-space:nowrap}.campaigns-page .campaigns-table-wrap .table tbody td{padding:10px 16px;vertical-align:middle;border-bottom:1px solid #f1f5f9;font-size:13px}.campaigns-page .campaigns-table-wrap .table tbody tr:hover{background:#f8fafc}.campaigns-page .campaigns-table-wrap .dt-layout-row{padding:14px 20px;margin:0 !important;border-top:1px solid #f1f5f9}.campaigns-page .campaigns-table-wrap .dt-layout-row:first-child{display:none}.campaigns-page .campaigns-table-wrap .dt-info{font-size:13px;color:#8899a6}.campaigns-page .campaigns-table-wrap .dt-paging .pagination{margin:0;padding:0;list-style:none;display:flex;align-items:center;gap:6px}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item .page-link{display:inline-flex;align-items:center;justify-content:center;min-width:36px;width:-moz-fit-content;width:fit-content;height:36px;padding:0 14px;border-radius:8px;font-size:13px;font-weight:500;border:1px solid #e2e8f0;background:#fff;color:#4e5e6a;cursor:pointer;transition:all .2s;text-decoration:none;line-height:1;white-space:nowrap}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item .page-link:hover{background:#eef2ff;color:#6690f4;border-color:#6690f4}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item.active .page-link{background:#6690f4;color:#fff;border-color:#6690f4;font-weight:600}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item.disabled .page-link{opacity:.35;cursor:default;pointer-events:none}.campaigns-page .campaigns-table-wrap .dt-processing{background:hsla(0,0%,100%,.9);color:#4e5e6a;font-size:14px}.campaigns-page .delete-history-entry{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:6px;border:none;cursor:pointer;font-size:12px;background:#fff5f5;color:#c00;transition:all .2s}.campaigns-page .delete-history-entry:hover{background:#c00;color:#fff}.products-page .products-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.products-page .products-header h2{margin:0;font-size:20px;font-weight:600;color:#2d3748}.products-page .products-header h2 i{color:#6690f4;margin-right:8px}.products-page .products-filters{display:flex;flex-wrap:wrap;align-items:flex-end;gap:20px;margin-bottom:16px}.products-page .products-filters .filter-group{flex:1 1 220px;min-width:0}.products-page .products-filters .filter-group label{display:block;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;margin-bottom:6px}.products-page .products-filters .filter-group label i{margin-right:4px}.products-page .products-filters .filter-group .form-control{width:100%;padding:10px 14px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;color:#2d3748;background:#fff;transition:border-color .2s}.products-page .products-filters .filter-group .form-control:focus{outline:none;border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.products-page .products-filters .filter-group select.form-control{-moz-appearance:none;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}.products-page .products-filters .filter-group.filter-group-client,.products-page .products-filters .filter-group.filter-group-campaign,.products-page .products-filters .filter-group.filter-group-ad-group{flex:1 1 260px}.products-page .products-filters .filter-group.filter-group-roas{flex:0 0 200px}.products-page .products-filters .filter-group.filter-group-columns{flex:0 0 240px}.products-page .products-actions{margin-bottom:12px}.products-page .products-actions .btn-danger{padding:7px 14px;font-size:13px;border-radius:6px;border:none;cursor:pointer;transition:all .2s}.products-page .products-actions .btn-danger:disabled{opacity:.4;cursor:default}.products-page .products-table-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow:hidden}.products-page .products-table-wrap .table{margin:0;width:100% !important}.products-page .products-table-wrap .table thead th{background:#f8fafc;border-bottom:2px solid #e2e8f0;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:#8899a6;padding:10px 8px;white-space:nowrap}.products-page .products-table-wrap .table tbody td{padding:6px 8px;vertical-align:middle;border-bottom:1px solid #f1f5f9;font-size:12px}.products-page .products-table-wrap .table tbody tr:hover{background:#f8fafc}.products-page .products-table-wrap .table input.min_roas,.products-page .products-table-wrap .table input.form-control-sm,.products-page .products-table-wrap .table select.custom_label_4,.products-page .products-table-wrap .table select.form-control-sm{padding:3px 6px;font-size:12px;border:1px solid #e2e8f0;border-radius:4px;background:#fff}.products-page .products-table-wrap .dt-layout-row{padding:14px 20px;margin:0 !important;border-top:1px solid #f1f5f9}.products-page .products-table-wrap .dt-layout-row:first-child{display:none}.products-page .products-table-wrap .dt-info{font-size:13px;color:#8899a6}.products-page .products-table-wrap .dt-paging .pagination{margin:0;padding:0;list-style:none;display:flex;align-items:center;gap:6px}.products-page .products-table-wrap .dt-paging .pagination .page-item .page-link{display:inline-flex;align-items:center;justify-content:center;min-width:36px;height:36px;padding:0 14px;border-radius:8px;font-size:13px;font-weight:500;border:1px solid #e2e8f0;background:#fff;color:#4e5e6a;cursor:pointer;transition:all .2s;text-decoration:none;line-height:1;white-space:nowrap}.products-page .products-table-wrap .dt-paging .pagination .page-item .page-link:hover{background:#eef2ff;color:#6690f4;border-color:#6690f4}.products-page .products-table-wrap .dt-paging .pagination .page-item.active .page-link{background:#6690f4;color:#fff;border-color:#6690f4;font-weight:600}.products-page .products-table-wrap .dt-paging .pagination .page-item.disabled .page-link{opacity:.35;cursor:default;pointer-events:none}.products-page .products-table-wrap .dt-processing{background:hsla(0,0%,100%,.9);color:#4e5e6a;font-size:14px}.products-page .delete-product{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;border:none;cursor:pointer;font-size:12px;background:#fff5f5;color:#c00;transition:all .2s}.products-page .delete-product:hover{background:#c00;color:#fff}.products-page .edit-product-title{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;border:none;cursor:pointer;font-size:12px;background:#eef2ff;color:#6690f4;transition:all .2s}.products-page .edit-product-title:hover{background:#6690f4;color:#fff}.desc-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}.desc-header label{margin:0}.desc-tabs{display:flex;gap:2px;background:#eee;border-radius:6px;padding:2px}.desc-tab{border:none;background:rgba(0,0,0,0);padding:4px 12px;font-size:12px;border-radius:4px;cursor:pointer;color:#666;transition:all .15s ease}.desc-tab i{margin-right:4px}.desc-tab.active{background:#fff;color:#333;box-shadow:0 1px 3px rgba(0,0,0,.12);font-weight:500}.desc-tab:hover:not(.active){color:#333}.desc-wrap{flex:1;min-width:0}.desc-preview ul,.desc-preview ol{margin:6px 0;padding-left:20px}.desc-preview li{margin-bottom:3px}.desc-preview b,.desc-preview strong{font-weight:600}.input-with-ai{display:flex;gap:8px;align-items:flex-start}.input-with-ai .form-control{flex:1}.btn-ai-suggest{display:inline-flex;align-items:center;gap:4px;padding:6px 12px;border-radius:8px;border:1px solid #c084fc;background:linear-gradient(135deg, #F3E8FF, #EDE9FE);color:#7c3aed;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s;white-space:nowrap;min-height:38px}.btn-ai-suggest i{font-size:13px}.btn-ai-suggest:hover{background:linear-gradient(135deg, #7C3AED, #6D28D9);color:#fff;border-color:#6d28d9}.btn-ai-suggest:disabled{opacity:.7;cursor:wait}.btn-ai-suggest.btn-ai-claude{border-color:#d97706;background:linear-gradient(135deg, #FEF3C7, #FDE68A);color:#92400e}.btn-ai-suggest.btn-ai-claude:hover{background:linear-gradient(135deg, #D97706, #B45309);color:#fff;border-color:#b45309}.form_container{background:#fff;padding:25px;max-width:1300px;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.06)}.form_container.full{max-width:100%}.form_container .form_group{margin-bottom:12px;display:flex}.form_container .form_group>.label{width:300px;display:inline-flex;align-items:flex-start;justify-content:right;padding-right:12px}.form_container .form_group .input{width:calc(100% - 300px)}.default_popup{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.45);display:none;z-index:2000}.default_popup .popup_content{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);background:#fff;padding:25px;border-radius:10px;max-width:1140px;width:95%;box-shadow:0 20px 60px rgba(0,0,0,.15)}.default_popup .popup_content .popup_header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px}.default_popup .popup_content .popup_header .title{font-size:18px;font-weight:600}.default_popup .popup_content .close{cursor:pointer;color:#a0aec0;font-size:18px;padding:4px}.default_popup .popup_content .close:hover{color:#c00}.dt-layout-table{margin-bottom:20px}.pagination button{border:1px solid #e2e8f0;background:#fff;display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;margin:0 2px;border-radius:4px;transition:all .2s;cursor:pointer}.pagination button:hover{background:#f4f6f9;border-color:#6690f4}table#products a{color:inherit;text-decoration:none}table#products .table-product-title{display:flex;justify-content:space-between}table#products .edit-product-title{display:flex;height:25px;align-items:center;justify-content:center;width:25px;cursor:pointer;background:#fff;border:1px solid #cbd5e0;color:#cbd5e0;border-radius:4px}table#products .edit-product-title:hover{background:#cbd5e0;color:#fff}table#products a.custom_name{color:#57b951 !important}.product-history-page .product-history-meta{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}.product-history-page .product-history-meta span{display:inline-flex;align-items:center;padding:5px 10px;border-radius:999px;font-size:12px;font-weight:600;color:#4e5e6a;background:#eef2ff;border:1px solid #d9e2ff}.product-history-page .product-history-chart-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);padding:20px;margin-bottom:16px}.product-history-page .chart-with-form{display:flex;gap:20px;align-items:flex-start}.product-history-page .chart-area{flex:1 1 auto;min-width:0}.product-history-page .product-history-chart{min-height:360px}.product-history-page .comment-form{width:340px;flex:0 0 340px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:14px}.product-history-page .comment-form .form-group{margin-bottom:12px}.product-history-page .comment-form label{display:block;font-weight:600;margin-bottom:6px;font-size:13px;color:#52606d}.product-history-page .comment-form input[type=date],.product-history-page .comment-form textarea{width:100%;border:1px solid #e2e8f0;border-radius:6px;padding:8px 12px;font-size:14px;font-family:"Roboto",sans-serif;background:#fff}.product-history-page .comment-form input[type=date]:focus,.product-history-page .comment-form textarea:focus{outline:none;border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.product-history-page .comment-form textarea{min-height:110px;resize:vertical}.product-history-page .comment-form .btn{width:100%;justify-content:center;padding:10px 16px}.product-history-page .comment-form .btn[disabled]{opacity:.6;cursor:not-allowed}.product-history-page .products-table-wrap{overflow-x:auto}.product-history-page .products-table-wrap .table{min-width:980px}.product-history-page .products-table-wrap .comment-cell{display:flex;align-items:center;justify-content:space-between;gap:10px}.product-history-page .products-table-wrap .comment-text{word-break:break-word}.product-history-page .products-table-wrap .delete-comment{color:#c00;text-decoration:none;font-weight:600;white-space:nowrap}.product-history-page .products-table-wrap .delete-comment:hover{text-decoration:underline}.product-history-page .products-table-wrap .dt-paging .pagination .page-item{list-style:none}.cron-status-overview{display:flex;flex-wrap:wrap;gap:10px 20px;margin-bottom:20px;color:#4e5e6a;font-size:13px}.cron-progress-list{margin-bottom:20px}.cron-progress-item{margin-bottom:14px}.cron-progress-item:last-child{margin-bottom:0}.cron-progress-item .cron-progress-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:6px;font-size:13px}.cron-progress-item .cron-progress-head strong{color:#2d3748;font-weight:600}.cron-progress-item .cron-progress-head span{color:#6b7a89;font-size:12px;font-weight:600;white-space:nowrap}.cron-progress-item small{display:block;margin-top:5px;color:#789;font-size:12px}.cron-progress-bar{width:100%;height:10px;border-radius:999px;background:#e9eef5;overflow:hidden}.cron-progress-bar>span{display:block;height:100%;background:linear-gradient(90deg, #5A9BFF 0%, #2E6BDF 100%)}.cron-url-list{margin-bottom:20px}.cron-url-item{border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;padding:10px 12px;margin-bottom:10px}.cron-url-item:last-child{margin-bottom:0}.cron-url-item .cron-url-top{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:6px}.cron-url-item .cron-url-top strong{color:#2d3748;font-size:13px;font-weight:600}.cron-url-item .cron-url-top small{color:#7a8794;font-size:11px;white-space:nowrap}.cron-url-item code{display:block;background:#eef2f7;border:1px solid #dde4ed;border-radius:6px;padding:6px 8px;color:#2e3b49;font-size:12px;overflow-x:auto}@media(max-width: 1200px){.product-history-page .chart-with-form{flex-direction:column}.product-history-page .comment-form{width:100%;flex:1 1 auto}}.jconfirm-box .form-group .select2-container,.adspro-dialog-box .form-group .select2-container{width:100% !important;margin-top:8px}.jconfirm-box .select2-container--default .select2-selection--single,.adspro-dialog-box .select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #e2e8f0;border-radius:6px;min-height:42px;display:flex;align-items:center;padding:4px 12px;box-shadow:none;transition:border-color .2s,box-shadow .2s;font-size:14px}.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__rendered,.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__rendered{padding-left:0;line-height:1.4;color:#495057}.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__placeholder,.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__placeholder{color:#cbd5e0}.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__arrow,.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__arrow{height:100%;right:8px}.jconfirm-box .select2-container--default.select2-container--focus .select2-selection--single,.jconfirm-box .select2-container--default .select2-selection--single:hover,.adspro-dialog-box .select2-container--default.select2-container--focus .select2-selection--single,.adspro-dialog-box .select2-container--default .select2-selection--single:hover{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1);outline:0}.jconfirm-box .select2-container .select2-dropdown,.adspro-dialog-box .select2-container .select2-dropdown{border-color:#e2e8f0;border-radius:0 0 6px 6px;font-size:14px}.jconfirm-box .select2-container .select2-search--dropdown .select2-search__field,.adspro-dialog-box .select2-container .select2-search--dropdown .select2-search__field{padding:6px 10px;border-radius:4px;border:1px solid #e2e8f0;font-size:14px}.jconfirm-box .select2-container--default .select2-results__option--highlighted[aria-selected],.adspro-dialog-box .select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#6690f4;color:#fff}@media(max-width: 992px){.sidebar{transform:translateX(-100%)}.sidebar.mobile-open{transform:translateX(0)}.main-wrapper{margin-left:0 !important}}.campaign-terms-wrap{display:flex;flex-direction:column;gap:20px;margin-top:20px}.campaign-terms-page{max-width:100%;overflow:hidden}.campaign-terms-page .campaigns-filters{flex-wrap:wrap}.campaign-terms-page .campaigns-filters .filter-group{min-width:220px}.campaign-terms-page .campaigns-filters .filter-group.terms-columns-group{min-width:280px}.campaign-terms-page .terms-card-toggle{margin-left:auto;width:28px;height:28px;border:1px solid #e2e8f0;border-radius:6px;background:#fff;color:#475569;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:all .2s}.campaign-terms-page .terms-card-toggle:hover{background:#f8fafc;border-color:#cbd5e1}.campaign-terms-page .terms-adgroups-card.is-collapsed .campaigns-extra-table-wrap{display:none}.campaign-terms-page .terms-search-toolbar{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid #eef2f7;background:#fff}.campaign-terms-page .terms-search-toolbar label{font-size:12px;font-weight:600;color:#475569;display:inline-flex;align-items:center;gap:6px;margin:0;white-space:nowrap}.campaign-terms-page .terms-search-toolbar .terms-search-toolbar-label{min-width:86px}.campaign-terms-page .terms-search-toolbar #terms_min_clicks_all,.campaign-terms-page .terms-search-toolbar #terms_max_clicks_all{width:160px;height:32px}.campaign-terms-page .terms-search-toolbar #terms_min_conversions_all,.campaign-terms-page .terms-search-toolbar #terms_max_conversions_all{width:130px;max-width:130px}.campaign-terms-page .terms-search-selected-label{margin:0;font-size:12px;color:#475569;font-weight:600;white-space:nowrap}.campaign-terms-page .terms-ai-analyze-btn{margin-left:auto;display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border-radius:6px;border:1px solid #bfdbfe;background:#eff6ff;color:#1d4ed8;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s}.campaign-terms-page .terms-ai-analyze-btn:hover{background:#dbeafe;border-color:#93c5fd}.campaign-terms-page .terms-ai-analyze-btn:disabled{opacity:.6;cursor:wait}.campaign-terms-page .terms-negative-toolbar{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid #eef2f7;background:#fff}.campaign-terms-page .terms-negative-bulk-btn{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border-radius:6px;border:1px solid #fecaca;background:#fef2f2;color:#dc2626;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s}.campaign-terms-page .terms-negative-bulk-btn:hover{background:#fee2e2;border-color:#fca5a5}.campaign-terms-page .terms-negative-bulk-btn:disabled{opacity:.5;cursor:not-allowed}.campaign-terms-page table.campaigns-extra-table>thead>tr>th{position:sticky;top:0;z-index:2;background-color:#111827 !important;color:#e5e7eb !important;border-bottom:1px solid #0b1220 !important;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;padding:10px 12px;white-space:nowrap}.campaign-terms-page #terms_search_table thead th .dt-column-order,.campaign-terms-page #terms_negative_table thead th .dt-column-order{display:none !important}.campaign-terms-page #terms_search_table thead th.dt-orderable-asc,.campaign-terms-page #terms_search_table thead th.dt-orderable-desc,.campaign-terms-page #terms_negative_table thead th.dt-orderable-asc,.campaign-terms-page #terms_negative_table thead th.dt-orderable-desc{cursor:pointer;padding-right:34px;overflow:hidden}.campaign-terms-page #terms_search_table thead th .dt-column-title,.campaign-terms-page #terms_negative_table thead th .dt-column-title{display:block;overflow:hidden;text-overflow:ellipsis;padding-right:2px}.campaign-terms-page #terms_search_table thead th.dt-orderable-asc::after,.campaign-terms-page #terms_search_table thead th.dt-orderable-desc::after,.campaign-terms-page #terms_negative_table thead th.dt-orderable-asc::after,.campaign-terms-page #terms_negative_table thead th.dt-orderable-desc::after{content:"↕";position:absolute;right:10px;top:50%;transform:translateY(-50%);width:16px;height:16px;border-radius:999px;font-size:12px;font-weight:700;line-height:16px;text-align:center;color:#e5e7eb;background:#374151}.campaign-terms-page #terms_search_table thead th.dt-ordering-asc::after,.campaign-terms-page #terms_negative_table thead th.dt-ordering-asc::after,.campaign-terms-page #terms_search_table thead th[aria-sort=ascending]::after,.campaign-terms-page #terms_negative_table thead th[aria-sort=ascending]::after{content:"▲";color:#fff;background:#2563eb}.campaign-terms-page #terms_search_table thead th.dt-ordering-desc::after,.campaign-terms-page #terms_negative_table thead th.dt-ordering-desc::after,.campaign-terms-page #terms_search_table thead th[aria-sort=descending]::after,.campaign-terms-page #terms_negative_table thead th[aria-sort=descending]::after{content:"▼";color:#fff;background:#2563eb}.campaign-terms-page #terms_negative_select_all,.campaign-terms-page .terms-negative-select-row,.campaign-terms-page #terms_search_select_all,.campaign-terms-page .terms-search-select-row{width:14px;height:14px;cursor:pointer}.campaign-terms-page .dt-layout-row:first-child{display:none}.campaign-terms-page .dt-layout-row{padding:10px 12px;margin:0 !important;border-top:1px solid #f1f5f9}.campaign-terms-page .dt-info{font-size:12px;color:#64748b}.campaign-terms-page .dt-paging .pagination{margin:0;padding:0;list-style:none !important;display:flex;align-items:center;gap:6px}.campaign-terms-page .dt-paging .pagination .page-item{list-style:none !important}.campaign-terms-page .dt-paging .pagination .page-item .page-link{display:inline-flex;align-items:center;justify-content:center;min-width:36px;width:-moz-fit-content;width:fit-content;height:32px;padding:0 12px;border-radius:6px;font-size:12px;font-weight:500;border:1px solid #e2e8f0;background:#fff;color:#4e5e6a;text-decoration:none;line-height:1;white-space:nowrap}.campaign-terms-page .dt-paging .pagination .page-item .page-link:hover{background:#eef2ff;color:#6690f4;border-color:#6690f4}.campaign-terms-page .dt-paging .pagination .page-item.previous .page-link,.campaign-terms-page .dt-paging .pagination .page-item.next .page-link{min-width:72px}.campaign-terms-page .dt-paging .pagination .page-item.active .page-link{background:#6690f4;color:#fff;border-color:#6690f4}.campaign-terms-page .dt-paging .pagination .page-item.disabled .page-link{opacity:.35;cursor:default;pointer-events:none}.terms-columns-box{display:flex;flex-direction:column;gap:6px}.terms-columns-control{border:1px solid #e2e8f0;border-radius:6px;background:#fff;overflow:hidden}.terms-columns-control summary{cursor:pointer;padding:8px 10px;font-size:12px;font-weight:600;color:#334155;list-style:none}.terms-columns-control summary::-webkit-details-marker{display:none}.terms-columns-control summary::after{content:"▼";float:right;font-size:10px;color:#64748b;margin-top:2px}.terms-columns-control[open] summary::after{content:"▲"}.terms-columns-list{border-top:1px solid #eef2f7;padding:8px 10px;max-height:180px;overflow-y:auto}.terms-columns-list .terms-col-item{display:flex;align-items:center;gap:8px;font-size:12px;color:#334155;margin-bottom:6px}.terms-columns-list .terms-col-item:last-child{margin-bottom:0}.terms-columns-list .terms-col-item input[type=checkbox]{margin:0}.campaigns-extra-card{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow:hidden}.campaigns-extra-card-title{padding:14px 16px;border-bottom:1px solid #e2e8f0;font-size:13px;font-weight:700;color:#334155;display:flex;align-items:center;gap:8px}.campaigns-extra-card-title .terms-card-title-label{display:inline-flex;align-items:center;gap:8px}.campaigns-extra-table-wrap{overflow:auto}.campaigns-extra-table{margin:0;width:100%;table-layout:fixed}.campaigns-extra-table tbody td{padding:9px 12px;border-bottom:1px solid #f1f5f9;font-size:13px;color:#334155;vertical-align:middle;white-space:nowrap}.campaigns-extra-table td.num-cell{text-align:right;white-space:nowrap}.campaigns-extra-table td.text-cell{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.campaigns-extra-table th.terms-negative-select-cell,.campaigns-extra-table td.terms-negative-select-cell,.campaigns-extra-table th.terms-search-select-cell,.campaigns-extra-table td.terms-search-select-cell{text-align:center}.campaigns-extra-table th.phrase-nowrap,.campaigns-extra-table td.phrase-nowrap{white-space:nowrap !important;overflow:hidden;text-overflow:ellipsis}.campaigns-extra-table .terms-add-negative-btn,.campaigns-extra-table .terms-remove-negative-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;cursor:pointer;transition:all .2s}.campaigns-extra-table .terms-add-negative-btn{border:1px solid #e2e8f0;background:#eef2ff;color:#3b82f6}.campaigns-extra-table .terms-add-negative-btn:hover{background:#3b82f6;color:#fff;border-color:#3b82f6}.campaigns-extra-table .terms-remove-negative-btn{border:1px solid #fecaca;background:#fef2f2;color:#dc2626}.campaigns-extra-table .terms-remove-negative-btn:hover{background:#dc2626;color:#fff;border-color:#dc2626}.campaigns-extra-table tbody tr:hover{background:#f8fafc}.campaigns-extra-table tbody tr.term-is-negative td{color:#dc2626}.campaigns-extra-table tbody tr.term-is-negative:hover{background:#fef2f2}.campaigns-empty-row{text-align:center;color:#94a3b8 !important;font-style:italic}.terms-ai-modal-toolbar{display:flex;align-items:center;gap:10px;margin-bottom:10px}.terms-ai-modal-toolbar label{font-size:12px;font-weight:600;color:#334155;margin:0}.terms-ai-modal-toolbar .form-control{width:200px;height:32px}.terms-ai-summary{font-size:12px;color:#64748b;margin-bottom:10px}.terms-ai-results-wrap{border:1px solid #e2e8f0;border-radius:8px;max-height:420px;overflow:auto}.terms-ai-results-table{width:100%;border-collapse:collapse;font-size:12px}.terms-ai-results-table th,.terms-ai-results-table td{border-bottom:1px solid #eef2f7;padding:8px;vertical-align:middle}.terms-ai-results-table th{position:sticky;top:0;background:#f8fafc;color:#334155;font-weight:700}.terms-ai-results-table td.term-col{min-width:260px;max-width:380px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.terms-ai-results-table td.reason-col{min-width:320px}.terms-ai-action-badge{display:inline-flex;align-items:center;justify-content:center;border-radius:999px;padding:2px 8px;font-size:11px;font-weight:700}.terms-ai-action-badge.action-exclude{background:#fee2e2;color:#b91c1c}.terms-ai-action-badge.action-keep{background:#dcfce7;color:#166534}.products-page .products-filters .filter-group.filter-group-columns{min-width:240px}.products-columns-control{border:1px solid #e2e8f0;border-radius:6px;background:#fff;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:"▼";float:right;font-size:10px;color:#64748b;margin-top:2px}.products-columns-control[open] summary::after{content:"▲"}.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}#products th:last-child,#products td:last-child{white-space:nowrap}#products .products-row-actions{display:inline-flex;align-items:center;gap:4px}#products .products-row-actions .btn{width:38px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;border-radius:4px !important}#products .products-row-actions .btn i{line-height:1}.products-page table#products>thead>tr>th{position:sticky;top:0;z-index:2;background-color:#111827 !important;color:#e5e7eb !important;border-bottom:1px solid #0b1220 !important;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;padding:10px 12px;white-space:nowrap}.products-page #products thead th .dt-column-order{display:none !important}.products-page #products thead th.dt-orderable-asc,.products-page #products thead th.dt-orderable-desc{cursor:pointer;padding-right:34px;overflow:hidden}.products-page #products thead th .dt-column-title{display:block;overflow:hidden;text-overflow:ellipsis;padding-right:2px}.products-page #products thead th.dt-orderable-asc::after,.products-page #products thead th.dt-orderable-desc::after{content:"↕";position:absolute;right:10px;top:50%;transform:translateY(-50%);width:16px;height:16px;border-radius:999px;font-size:12px;font-weight:700;line-height:16px;text-align:center;color:#e5e7eb;background:#374151}.products-page #products thead th.dt-ordering-asc::after,.products-page #products thead th[aria-sort=ascending]::after{content:"▲";color:#fff;background:#2563eb}.products-page #products thead th.dt-ordering-desc::after,.products-page #products thead th[aria-sort=descending]::after{content:"▼";color:#fff;background:#2563eb}/*# sourceMappingURL=style.css.map */ \ No newline at end of file +*{box-sizing:border-box}body{font-family:"Roboto",sans-serif;margin:0;padding:0;font-size:14px;color:#4e5e6a;background:#f4f6f9;max-width:100vw;overflow-x:hidden}.hide{display:none}small{font-size:.75em}.text-right{text-align:right}.text-bold{font-weight:700 !important}.nowrap{white-space:nowrap}body.unlogged{background:#f4f6f9;margin:0;padding:0}.login-container{display:flex;min-height:100vh}.login-brand{flex:0 0 45%;background:linear-gradient(135deg, #1E2A3A 0%, #2C3E57 50%, #6690F4 100%);display:flex;align-items:center;justify-content:center;padding:60px;position:relative;overflow:hidden}.login-brand::before{content:"";position:absolute;top:-50%;right:-50%;width:100%;height:100%;background:radial-gradient(circle, rgba(102, 144, 244, 0.15) 0%, transparent 70%);border-radius:50%}.login-brand .brand-content{position:relative;z-index:1;color:#fff;max-width:400px}.login-brand .brand-logo{font-size:48px;font-weight:300;margin-bottom:20px;letter-spacing:-1px}.login-brand .brand-logo strong{font-weight:700}.login-brand .brand-tagline{font-size:18px;opacity:.85;line-height:1.6;margin-bottom:50px}.login-brand .brand-features .feature{display:flex;align-items:center;gap:15px;margin-bottom:20px;opacity:.8}.login-brand .brand-features .feature i{font-size:20px;width:40px;height:40px;display:flex;align-items:center;justify-content:center;background:hsla(0,0%,100%,.1);border-radius:10px}.login-brand .brand-features .feature span{font-size:15px}.login-form-wrapper{flex:1;display:flex;align-items:center;justify-content:center;padding:60px;background:#fff}.login-box{width:100%;max-width:420px}.login-box .login-header{margin-bottom:35px}.login-box .login-header h1{font-size:28px;font-weight:700;color:#2d3748;margin:0 0 8px}.login-box .login-header p{color:#718096;font-size:15px;margin:0}.login-box .form-group{margin-bottom:20px}.login-box .form-group label{display:block;font-size:13px;font-weight:600;color:#2d3748;margin-bottom:6px}.login-box .input-with-icon{position:relative}.login-box .input-with-icon i{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#a0aec0;font-size:14px}.login-box .input-with-icon .form-control{padding-left:42px}.login-box .form-control{width:100%;height:46px;border:2px solid #e2e8f0;border-radius:8px;padding:0 14px;font-size:14px;font-family:"Roboto",sans-serif;color:#2d3748;transition:border-color .3s,box-shadow .3s}.login-box .form-control::-moz-placeholder{color:#cbd5e0}.login-box .form-control::placeholder{color:#cbd5e0}.login-box .form-control:focus{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.15);outline:none}.login-box .form-error{color:#c00;font-size:12px;margin-top:4px}.login-box .checkbox-group .checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:#718096;font-weight:400}.login-box .checkbox-group .checkbox-label input[type=checkbox]{width:16px;height:16px;accent-color:#6690f4}.login-box .btn-login{width:100%;height:48px;font-size:15px;font-weight:600;border-radius:8px;display:flex;align-items:center;justify-content:center;gap:8px}.login-box .btn-login.disabled{opacity:.7;pointer-events:none}.login-box .alert{display:none;padding:12px 16px;border-radius:8px;font-size:13px;margin-bottom:20px}.login-box .alert.alert-danger{background:#fff5f5;color:#c00;border:1px solid #fed7d7}.login-box .alert.alert-success{background:#f0fff4;color:#276749;border:1px solid #c6f6d5}@media(max-width: 768px){.login-brand{display:none}.login-form-wrapper{padding:30px 20px}}body.logged{display:flex;min-height:100vh;background:#f4f6f9}.sidebar{width:260px;min-height:100vh;background:#1e2a3a;position:fixed;top:0;left:0;z-index:1000;display:flex;flex-direction:column;transition:width .3s ease;overflow:hidden}.sidebar.collapsed{width:70px}.sidebar.collapsed .sidebar-header{padding:16px 0;justify-content:center}.sidebar.collapsed .sidebar-header .sidebar-logo{display:none}.sidebar.collapsed .sidebar-header .sidebar-toggle i{transform:rotate(180deg)}.sidebar.collapsed .sidebar-nav ul li a{padding:12px 0;justify-content:center}.sidebar.collapsed .sidebar-nav ul li a span{display:none}.sidebar.collapsed .sidebar-nav ul li a i{margin-right:0;font-size:18px}.sidebar.collapsed .sidebar-nav ul li.nav-group .nav-group-label{padding:12px 0;justify-content:center}.sidebar.collapsed .sidebar-nav ul li.nav-group .nav-group-label span{display:none}.sidebar.collapsed .sidebar-nav ul li.nav-group .nav-group-label i{margin-right:0;font-size:18px}.sidebar.collapsed .sidebar-footer .sidebar-user{justify-content:center}.sidebar.collapsed .sidebar-footer .sidebar-user .user-info{display:none}.sidebar.collapsed .sidebar-footer .sidebar-logout{justify-content:center}.sidebar.collapsed .sidebar-footer .sidebar-logout span{display:none}.sidebar.collapsed .nav-divider{margin:8px 15px}.sidebar-header{display:flex;align-items:center;justify-content:space-between;padding:20px 20px 16px;border-bottom:1px solid hsla(0,0%,100%,.08)}.sidebar-header .sidebar-logo a{color:#fff;text-decoration:none;font-size:24px;font-weight:300;letter-spacing:-0.5px}.sidebar-header .sidebar-logo a strong{font-weight:700}.sidebar-header .sidebar-toggle{background:none;border:none;color:#a8b7c7;cursor:pointer;padding:6px;border-radius:6px;transition:all .3s}.sidebar-header .sidebar-toggle:hover{background:hsla(0,0%,100%,.08);color:#fff}.sidebar-header .sidebar-toggle i{transition:transform .3s}.sidebar-nav{flex:1;padding:12px 0;overflow-y:auto}.sidebar-nav ul{list-style:none;margin:0;padding:0}.sidebar-nav ul li.nav-group{margin-bottom:4px}.sidebar-nav ul li.nav-group .nav-group-label{display:flex;align-items:center;padding:11px 20px;color:#d5deea;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;border-left:3px solid rgba(0,0,0,0)}.sidebar-nav ul li.nav-group .nav-group-label i{width:20px;text-align:center;margin-right:12px;font-size:14px;color:#b6c4d3}.sidebar-nav ul li.nav-group .nav-submenu{margin:0;padding:0;list-style:none}.sidebar-nav ul li.nav-group .nav-submenu li a{padding-left:44px}.sidebar-nav ul li.nav-group.active>.nav-group-label{color:#fff;background:rgba(102,144,244,.12);border-left-color:#6690f4}.sidebar-nav ul li.nav-group.active>.nav-group-label i{color:#6690f4}.sidebar-nav ul li.nav-divider{height:1px;background:hsla(0,0%,100%,.08);margin:8px 20px}.sidebar-nav ul li a{display:flex;align-items:center;padding:11px 20px;color:#a8b7c7;text-decoration:none;font-size:14px;transition:all .2s;border-left:3px solid rgba(0,0,0,0)}.sidebar-nav ul li a i{width:20px;text-align:center;margin-right:12px;font-size:15px}.sidebar-nav ul li a:hover{background:#263548;color:#fff}.sidebar-nav ul li.active>a{background:rgba(102,144,244,.15);color:#fff;border-left-color:#6690f4}.sidebar-nav ul li.active>a i{color:#6690f4}.badge-alerts-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;margin-left:8px;border-radius:50%;font-size:11px;font-weight:600;line-height:1;background:#fff;color:#6690f4}.sidebar-footer{padding:16px 20px;border-top:1px solid hsla(0,0%,100%,.08)}.sidebar-footer .sidebar-user{display:flex;align-items:center;gap:10px;margin-bottom:12px}.sidebar-footer .sidebar-user .user-avatar{width:34px;height:34px;border-radius:50%;background:rgba(102,144,244,.2);display:flex;align-items:center;justify-content:center;color:#6690f4;font-size:14px;flex-shrink:0}.sidebar-footer .sidebar-user .user-info{overflow:hidden}.sidebar-footer .sidebar-user .user-info .user-email{color:#a8b7c7;font-size:12px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sidebar-footer .sidebar-logout{display:flex;align-items:center;gap:8px;color:#e53e3e;text-decoration:none;font-size:13px;padding:8px 10px;border-radius:6px;transition:all .2s}.sidebar-footer .sidebar-logout i{font-size:14px}.sidebar-footer .sidebar-logout:hover{background:rgba(229,62,62,.1)}.main-wrapper{margin-left:260px;flex:1;min-height:100vh;transition:margin-left .3s ease;display:flex;flex-direction:column}.main-wrapper.expanded{margin-left:70px}.topbar{height:56px;background:#fff;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;padding:0 25px;position:sticky;top:0;z-index:500}.topbar .topbar-toggle{background:none;border:none;color:#4e5e6a;cursor:pointer;padding:8px 10px;border-radius:6px;font-size:16px;margin-right:15px;transition:all .2s}.topbar .topbar-toggle:hover{background:#f4f6f9}.topbar .topbar-breadcrumb{font-size:16px;font-weight:600;color:#2d3748}.content{flex:1;padding:25px}.app-alert{background:#ebf8ff;border:1px solid #bee3f8;color:#2b6cb0;padding:12px 16px;border-radius:8px;margin-bottom:20px;font-size:14px}.btn{padding:10px 20px;transition:all .2s ease;color:#fff;border:0;border-radius:6px;cursor:pointer;display:inline-flex;text-decoration:none;gap:6px;justify-content:center;align-items:center;font-size:14px;font-family:"Roboto",sans-serif;font-weight:500}.btn.btn_small,.btn.btn-xs,.btn.btn-sm{padding:5px 10px;font-size:12px}.btn.btn_small i,.btn.btn-xs i,.btn.btn-sm i{font-size:11px}.btn.btn-success{background:#57b951}.btn.btn-success:hover{background:#4a9c3b}.btn.btn-primary{background:#6690f4}.btn.btn-primary:hover{background:#3164db}.btn.btn-danger{background:#c00}.btn.btn-danger:hover{background:#b30000}.btn.disabled{opacity:.6;pointer-events:none}.form-control{border:1px solid #e2e8f0;border-radius:6px;height:38px;width:100%;padding:6px 12px;font-family:"Roboto",sans-serif;font-size:14px;color:#2d3748;transition:border-color .2s,box-shadow .2s}.form-control option{padding:5px}.form-control:focus{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1);outline:none}input[type=checkbox]{border:1px solid #e2e8f0}table{border-collapse:collapse;font-size:13px}.table{width:100%}.table th,.table td{border:1px solid #e2e8f0;padding:8px 10px}.table th{background:#f7fafc;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.03em;color:#718096}.table td.center{text-align:center}.table td.left{text-align:left}.table.table-sm td{padding:5px !important}.table input.form-control{font-size:13px;height:32px}.card{background:#fff;padding:20px;border-radius:8px;color:#2d3748;font-size:14px;box-shadow:0 1px 3px rgba(0,0,0,.06)}.card.mb25{margin-bottom:20px}.card .card-header{font-weight:600;font-size:15px}.card .card-body{padding-top:12px}.card .card-body table th,.card .card-body table td{font-size:13px}.card .card-body table th.bold,.card .card-body table td.bold{font-weight:600}.card .card-body table th.text-right,.card .card-body table td.text-right{text-align:right}.card .card-body table th.text-center,.card .card-body table td.text-center{text-align:center}.action_menu{display:flex;margin-bottom:20px;gap:12px}.action_menu .btn{padding:8px 16px}.action_menu .btn.btn_add{background:#57b951}.action_menu .btn.btn_add:hover{background:#4a9c3b}.action_menu .btn.btn_cancel{background:#c00}.action_menu .btn.btn_cancel:hover{background:#b30000}.settings-tabs{display:flex;gap:8px;margin-bottom:18px}.settings-tabs .settings-tab{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:8px;text-decoration:none;color:#6b7a89;background:#e9eef5;border:1px solid #d8e0ea;font-size:13px;font-weight:600;transition:all .2s}.settings-tabs .settings-tab:hover{color:#2d3748;background:#dde6f2}.settings-tabs .settings-tab.active{color:#fff;background:#6690f4;border-color:#6690f4}.settings-card{background:#fff;border-radius:10px;padding:28px;box-shadow:0 1px 4px rgba(0,0,0,.06)}.settings-card .settings-card-header{display:flex;align-items:center;gap:14px;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid #e2e8f0}.settings-card .settings-card-header .settings-card-icon{width:44px;height:44px;border-radius:10px;background:rgb(225.706097561,233.7475609756,252.893902439);color:#6690f4;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}.settings-card .settings-card-header h3{margin:0;font-size:17px;font-weight:600;color:#2d3748}.settings-card .settings-card-header small{color:#8899a6;font-size:13px}.settings-card .settings-field{margin-bottom:18px}.settings-card .settings-field label{display:block;font-size:13px;font-weight:600;color:#2d3748;margin-bottom:6px}.settings-card .settings-input-wrap{position:relative}.settings-card .settings-input-wrap .settings-input-icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#a0aec0;font-size:14px;pointer-events:none}.settings-card .settings-input-wrap .form-control{padding-left:38px}.settings-card .settings-input-wrap .settings-toggle-pw{position:absolute;right:4px;top:50%;transform:translateY(-50%);background:none;border:none;color:#a0aec0;cursor:pointer;padding:6px 10px;font-size:14px;transition:color .2s}.settings-card .settings-input-wrap .settings-toggle-pw:hover{color:#6690f4}.settings-card .settings-field .settings-toggle-label{display:inline-flex;align-items:center;gap:10px;cursor:pointer;font-size:14px;font-weight:500;-webkit-user-select:none;-moz-user-select:none;user-select:none;margin-bottom:0;width:100%}.settings-card .settings-field .settings-toggle-label .settings-toggle-text{flex:1 1 auto;min-width:0;line-height:1.35}.settings-card .settings-toggle-checkbox{display:none}.settings-card .settings-toggle-checkbox+.settings-toggle-switch{display:inline-block;position:relative;width:44px;height:24px;background:#ccc;border-radius:12px;transition:background .2s;flex-shrink:0}.settings-card .settings-toggle-checkbox+.settings-toggle-switch::after{content:"";position:absolute;top:3px;left:3px;width:18px;height:18px;background:#fff;border-radius:50%;transition:transform .2s}.settings-card .settings-toggle-checkbox:checked+.settings-toggle-switch{background:#22c55e}.settings-card .settings-toggle-checkbox:checked+.settings-toggle-switch::after{transform:translateX(20px)}.settings-card .settings-fields-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 24px}@media(max-width: 768px){.settings-card .settings-fields-grid{grid-template-columns:1fr}}.settings-card .settings-alert-error{display:flex;align-items:center;gap:10px;background:#fff5f5;color:#c00;border:1px solid #fed7d7;border-radius:8px;padding:12px 16px;margin-bottom:20px;font-size:13px}.settings-card .settings-alert-error i{font-size:16px;flex-shrink:0}.clients-page .clients-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.clients-page .clients-header h2{margin:0;font-size:20px;font-weight:600;color:#2d3748}.clients-page .clients-header h2 i{color:#6690f4;margin-right:8px}.clients-page .clients-table-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow:hidden}.clients-page .clients-table-wrap .table{margin:0}.clients-page .clients-table-wrap .table thead th{background:#f8fafc;border-bottom:2px solid #e2e8f0;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;padding:14px 20px}.clients-page .clients-table-wrap .table tbody td{padding:14px 20px;vertical-align:middle;border-bottom:1px solid #f1f5f9}.clients-page .clients-table-wrap .table tbody tr:hover{background:#f8fafc}.clients-page .clients-table-wrap .table .client-id{color:#8899a6;font-size:13px;font-weight:600}.clients-page .clients-table-wrap .table .client-name{font-weight:600;color:#2d3748}.clients-page .badge-id{display:inline-block;background:#eef2ff;color:#6690f4;font-size:13px;font-weight:600;padding:4px 10px;border-radius:6px;font-family:monospace}.clients-page .actions-cell{text-align:center;white-space:nowrap}.clients-page .btn-icon{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:8px;border:none;cursor:pointer;font-size:14px;transition:all .2s;margin:0 2px}.clients-page .btn-icon.btn-icon-edit{background:#eef2ff;color:#6690f4}.clients-page .btn-icon.btn-icon-edit:hover{background:#6690f4;color:#fff}.clients-page .btn-icon.btn-icon-delete{background:#fff5f5;color:#c00}.clients-page .btn-icon.btn-icon-delete:hover{background:#c00;color:#fff}.clients-page .btn-icon.btn-icon-sync{background:#f0fdf4;color:#16a34a}.clients-page .btn-icon.btn-icon-sync:hover{background:#16a34a;color:#fff}.clients-page .btn-icon.btn-icon-sync:disabled{opacity:.7;cursor:wait}.clients-page .btn-icon.btn-icon-sync.is-queued{background:#fef3c7;color:#d97706}.clients-page .client-sync-bars{display:flex;flex-direction:column;gap:4px}.clients-page .client-sync-row{display:flex;align-items:center;gap:4px}.clients-page .client-sync-label{font-size:11px;font-weight:600;color:#8899a6;width:18px;flex-shrink:0}.clients-page .client-sync-track{flex:1;height:6px;border-radius:999px;background:#e9eef5;overflow:hidden}.clients-page .client-sync-fill{height:100%;border-radius:999px;background:#cbd5e0;transition:width .4s ease}.clients-page .client-sync-fill.is-active{background:linear-gradient(90deg, #5A9BFF 0%, #2E6BDF 100%)}.clients-page .client-sync-fill.is-done{background:#57b951}.clients-page .client-sync-pct{font-size:11px;font-weight:600;color:#8899a6;width:32px;text-align:right;flex-shrink:0}.clients-page .empty-state{text-align:center;padding:50px 20px !important;color:#a0aec0}.clients-page .empty-state i{font-size:40px;margin-bottom:12px;display:block}.clients-page .empty-state p{margin:0;font-size:15px}.btn-secondary{background:#e2e8f0;color:#2d3748;border:none;padding:8px 18px;border-radius:6px;font-size:14px;cursor:pointer;transition:background .2s}.btn-secondary:hover{background:#cbd5e0}.campaigns-page{max-width:100%;overflow-x:hidden;width:100%}.campaigns-page .campaigns-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.campaigns-page .campaigns-header h2{margin:0;font-size:20px;font-weight:600;color:#2d3748}.campaigns-page .campaigns-header h2 i{color:#6690f4;margin-right:8px}.campaigns-page .campaigns-filters{display:flex;flex-wrap:wrap;gap:20px;margin-bottom:20px}.campaigns-page .campaigns-filters .filter-group{flex:1;min-width:0}.campaigns-page .campaigns-filters .filter-group label{display:block;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;margin-bottom:6px}.campaigns-page .campaigns-filters .filter-group label i{margin-right:4px}.campaigns-page .campaigns-filters .filter-group .form-control{width:100%;padding:10px 14px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;color:#2d3748;background:#fff;transition:border-color .2s;-moz-appearance:none;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}.campaigns-page .campaigns-filters .filter-group .form-control:focus{outline:none;border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.campaigns-page .campaigns-filters .filter-group .filter-with-action{display:flex;align-items:center;gap:8px}.campaigns-page .campaigns-filters .filter-group .filter-with-action .form-control{flex:1}.campaigns-page .campaigns-filters .filter-group .filter-with-action .btn-icon{flex-shrink:0;width:42px;height:42px;display:inline-flex;align-items:center;justify-content:center;border-radius:8px;border:none;cursor:pointer;font-size:14px;transition:all .2s}.campaigns-page .campaigns-filters .filter-group .filter-with-action .btn-icon.btn-icon-delete{background:#fff5f5;color:#c00}.campaigns-page .campaigns-filters .filter-group .filter-with-action .btn-icon.btn-icon-delete:hover{background:#c00;color:#fff}.campaigns-page .campaigns-filters .filter-group-campaign-multi{flex:2 !important}.campaigns-page .campaigns-filters .campaign-dropdown{flex:1;min-width:0;position:relative}.campaigns-page .campaigns-filters .campaign-dropdown-trigger{width:100%;padding:10px 14px;padding-right:32px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;color:#2d3748;background:#fff;cursor:pointer;display:flex;align-items:center;transition:border-color .2s;position:relative;min-height:42px;box-sizing:border-box}.campaigns-page .campaigns-filters .campaign-dropdown-trigger .campaign-dropdown-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.campaigns-page .campaigns-filters .campaign-dropdown-trigger .campaign-dropdown-text.is-placeholder{color:#8899a6}.campaigns-page .campaigns-filters .campaign-dropdown-trigger .campaign-dropdown-arrow{position:absolute;right:12px;font-size:10px;color:#8899a6;transition:transform .2s}.campaigns-page .campaigns-filters .campaign-dropdown.is-open .campaign-dropdown-trigger{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.campaigns-page .campaigns-filters .campaign-dropdown.is-open .campaign-dropdown-arrow{transform:rotate(180deg)}.campaigns-page .campaigns-filters .campaign-dropdown.is-open .campaign-dropdown-menu{display:block}.campaigns-page .campaigns-filters .campaign-dropdown-menu{display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:100;max-height:280px;overflow-y:auto;background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.1);padding:4px 0}.campaigns-page .campaigns-filters .campaign-dropdown-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:14px;color:#2d3748;margin:0;transition:background .15s}.campaigns-page .campaigns-filters .campaign-dropdown-item:hover{background:#f8fafc}.campaigns-page .campaigns-filters .campaign-dropdown-item.is-checked{background:#eef2ff}.campaigns-page .campaigns-filters .campaign-dropdown-item input[type=checkbox]{width:16px;height:16px;cursor:pointer;flex-shrink:0;accent-color:#6690f4}.campaigns-page .campaigns-filters .campaign-dropdown-item span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.campaigns-page .campaigns-list-panel{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);margin-bottom:20px;overflow:hidden}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #e2e8f0;gap:12px}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left{display:flex;align-items:center;gap:8px;font-size:13px;color:#4e5e6a}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left input[type=checkbox]{width:16px;height:16px;cursor:pointer}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left label{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;margin:0}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left .campaigns-selected-count{margin-left:12px;color:#8899a6}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-right .campaigns-bulk-delete-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;background:#fff5f5;color:#c00;transition:all .2s}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-right .campaigns-bulk-delete-btn:hover:not(:disabled){background:#c00;color:#fff}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-right .campaigns-bulk-delete-btn:disabled{opacity:.4;cursor:not-allowed}.campaigns-page .campaigns-list-panel .campaigns-list-items{display:flex;flex-wrap:wrap;gap:0;padding:8px 8px;max-height:220px;overflow-y:auto}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item{display:flex;align-items:center;gap:8px;padding:6px 12px;margin:2px;border-radius:6px;font-size:13px;color:#2d3748;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background .15s}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item:hover{background:#f0f4ff}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item input[type=checkbox]{width:15px;height:15px;cursor:pointer;flex-shrink:0}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item .campaigns-list-item-name{white-space:nowrap}.campaigns-page .campaigns-chart-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);padding:20px;margin-bottom:20px;min-height:350px;overflow:hidden}.campaigns-page .campaigns-chart-wrap #container{max-width:100%}.campaigns-page .campaigns-table-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow-x:auto;-ms-overflow-style:none;scrollbar-width:none;max-width:100%}.campaigns-page .campaigns-table-wrap::-webkit-scrollbar{display:none}.campaigns-page .campaigns-table-wrap .table{margin:0;width:100% !important}.campaigns-page .campaigns-table-wrap .table thead th{background:#f8fafc;border-bottom:2px solid #e2e8f0;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;padding:12px 16px;white-space:nowrap}.campaigns-page .campaigns-table-wrap .table tbody td{padding:10px 16px;vertical-align:middle;border-bottom:1px solid #f1f5f9;font-size:13px}.campaigns-page .campaigns-table-wrap .table tbody tr:hover{background:#f8fafc}.campaigns-page .campaigns-table-wrap .dt-layout-row{padding:14px 20px;margin:0 !important;border-top:1px solid #f1f5f9}.campaigns-page .campaigns-table-wrap .dt-layout-row:first-child{display:none}.campaigns-page .campaigns-table-wrap .dt-info{font-size:13px;color:#8899a6}.campaigns-page .campaigns-table-wrap .dt-paging .pagination{margin:0;padding:0;list-style:none;display:flex;align-items:center;gap:6px}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item .page-link{display:inline-flex;align-items:center;justify-content:center;min-width:36px;width:-moz-fit-content;width:fit-content;height:36px;padding:0 14px;border-radius:8px;font-size:13px;font-weight:500;border:1px solid #e2e8f0;background:#fff;color:#4e5e6a;cursor:pointer;transition:all .2s;text-decoration:none;line-height:1;white-space:nowrap}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item .page-link:hover{background:#eef2ff;color:#6690f4;border-color:#6690f4}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item.active .page-link{background:#6690f4;color:#fff;border-color:#6690f4;font-weight:600}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item.disabled .page-link{opacity:.35;cursor:default;pointer-events:none}.campaigns-page .campaigns-table-wrap .dt-processing{background:hsla(0,0%,100%,.9);color:#4e5e6a;font-size:14px}.campaigns-page .delete-history-entry{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:6px;border:none;cursor:pointer;font-size:12px;background:#fff5f5;color:#c00;transition:all .2s}.campaigns-page .delete-history-entry:hover{background:#c00;color:#fff}.products-page .products-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.products-page .products-header h2{margin:0;font-size:20px;font-weight:600;color:#2d3748}.products-page .products-header h2 i{color:#6690f4;margin-right:8px}.products-page .products-filters{display:flex;flex-wrap:wrap;align-items:flex-end;gap:20px;margin-bottom:16px}.products-page .products-filters .filter-group{flex:1 1 220px;min-width:0}.products-page .products-filters .filter-group label{display:block;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;margin-bottom:6px}.products-page .products-filters .filter-group label i{margin-right:4px}.products-page .products-filters .filter-group .form-control{width:100%;padding:10px 14px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;color:#2d3748;background:#fff;transition:border-color .2s}.products-page .products-filters .filter-group .form-control:focus{outline:none;border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.products-page .products-filters .filter-group select.form-control{-moz-appearance:none;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}.products-page .products-filters .filter-group.filter-group-client,.products-page .products-filters .filter-group.filter-group-campaign,.products-page .products-filters .filter-group.filter-group-ad-group{flex:1 1 260px}.products-page .products-filters .filter-group.filter-group-ad-group .ad-group-filter-actions{display:flex;gap:8px;align-items:center}.products-page .products-filters .filter-group.filter-group-ad-group .ad-group-filter-actions .form-control{flex:1 1 auto;min-width:0}.products-page .products-filters .filter-group.filter-group-ad-group #delete-products-ad-group{min-width:38px;height:38px;border-radius:6px;margin:0;background:#dc3545;border:1px solid #dc3545;color:#fff;cursor:pointer}.products-page .products-filters .filter-group.filter-group-ad-group #delete-products-ad-group:hover:not(:disabled){background:#bb2d3b;border-color:#bb2d3b;color:#fff}.products-page .products-filters .filter-group.filter-group-ad-group #delete-products-ad-group:disabled{opacity:.45;cursor:default;background:#dc3545;border-color:#dc3545;color:#fff}.products-page .products-filters .filter-group.filter-group-roas{flex:0 0 200px}.products-page .products-filters .filter-group.filter-group-columns{flex:0 0 240px}.products-page .products-scope-alerts{margin-bottom:12px;border:1px solid #fecaca;background:#fef2f2;border-radius:8px;overflow:hidden}.products-page .products-scope-alerts summary{cursor:pointer;list-style:none;padding:10px 12px;font-size:13px;font-weight:600;color:#991b1b;display:flex;align-items:center;gap:8px}.products-page .products-scope-alerts summary::-webkit-details-marker{display:none}.products-page .products-scope-alerts .products-scope-alerts-list{border-top:1px solid #fecaca;background:#fff;max-height:260px;overflow:auto}.products-page .products-scope-alerts .products-scope-alert-item{padding:10px 12px;border-bottom:1px solid #f1f5f9}.products-page .products-scope-alerts .products-scope-alert-item:last-child{border-bottom:none}.products-page .products-scope-alerts .products-scope-alert-meta{display:flex;align-items:center;gap:8px;margin-bottom:4px;font-size:11px;color:#64748b}.products-page .products-scope-alerts .products-scope-alert-type{display:inline-flex;align-items:center;padding:2px 6px;border-radius:999px;background:#eef2ff;color:#4338ca;font-weight:600;text-transform:uppercase;letter-spacing:.3px}.products-page .products-scope-alerts .products-scope-alert-message{font-size:13px;color:#2d3748;line-height:1.45}.products-page .products-actions{margin-bottom:12px}.products-page .products-actions .btn-danger{padding:7px 14px;font-size:13px;border-radius:6px;border:none;cursor:pointer;transition:all .2s}.products-page .products-actions .btn-danger:disabled{opacity:.4;cursor:default}.products-page .products-table-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow:hidden}.products-page .products-table-wrap .table{margin:0;width:100% !important}.products-page .products-table-wrap .table thead th{background:#f8fafc;border-bottom:2px solid #e2e8f0;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:#8899a6;padding:10px 8px;white-space:nowrap}.products-page .products-table-wrap .table tbody td{padding:6px 8px;vertical-align:middle;border-bottom:1px solid #f1f5f9;font-size:12px}.products-page .products-table-wrap .table tbody tr:hover{background:#f8fafc}.products-page .products-table-wrap .table input.min_roas,.products-page .products-table-wrap .table input.form-control-sm,.products-page .products-table-wrap .table select.custom_label_4,.products-page .products-table-wrap .table select.form-control-sm{padding:3px 6px;font-size:12px;border:1px solid #e2e8f0;border-radius:4px;background:#fff}.products-page .products-table-wrap .dt-layout-row{padding:14px 20px;margin:0 !important;border-top:1px solid #f1f5f9}.products-page .products-table-wrap .dt-layout-row:first-child{display:none}.products-page .products-table-wrap .dt-info{font-size:13px;color:#8899a6}.products-page .products-table-wrap .dt-paging .pagination{margin:0;padding:0;list-style:none;display:flex;align-items:center;gap:6px}.products-page .products-table-wrap .dt-paging .pagination .page-item .page-link{display:inline-flex;align-items:center;justify-content:center;min-width:36px;height:36px;padding:0 14px;border-radius:8px;font-size:13px;font-weight:500;border:1px solid #e2e8f0;background:#fff;color:#4e5e6a;cursor:pointer;transition:all .2s;text-decoration:none;line-height:1;white-space:nowrap}.products-page .products-table-wrap .dt-paging .pagination .page-item .page-link:hover{background:#eef2ff;color:#6690f4;border-color:#6690f4}.products-page .products-table-wrap .dt-paging .pagination .page-item.active .page-link{background:#6690f4;color:#fff;border-color:#6690f4;font-weight:600}.products-page .products-table-wrap .dt-paging .pagination .page-item.disabled .page-link{opacity:.35;cursor:default;pointer-events:none}.products-page .products-table-wrap .dt-processing{background:hsla(0,0%,100%,.9);color:#4e5e6a;font-size:14px}.products-page .delete-product{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;border:none;cursor:pointer;font-size:12px;background:#fff5f5;color:#c00;transition:all .2s}.products-page .delete-product:hover{background:#c00;color:#fff}.products-page .edit-product-title{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;border:none;cursor:pointer;font-size:12px;background:#eef2ff;color:#6690f4;transition:all .2s}.products-page .edit-product-title:hover{background:#6690f4;color:#fff}.desc-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}.desc-header label{margin:0}.desc-tabs{display:flex;gap:2px;background:#eee;border-radius:6px;padding:2px}.desc-tab{border:none;background:rgba(0,0,0,0);padding:4px 12px;font-size:12px;border-radius:4px;cursor:pointer;color:#666;transition:all .15s ease}.desc-tab i{margin-right:4px}.desc-tab.active{background:#fff;color:#333;box-shadow:0 1px 3px rgba(0,0,0,.12);font-weight:500}.desc-tab:hover:not(.active){color:#333}.desc-wrap{flex:1;min-width:0}.desc-preview ul,.desc-preview ol{margin:6px 0;padding-left:20px}.desc-preview li{margin-bottom:3px}.desc-preview b,.desc-preview strong{font-weight:600}.input-with-ai{display:flex;gap:8px;align-items:flex-start}.input-with-ai .form-control{flex:1}.btn-ai-suggest{display:inline-flex;align-items:center;gap:4px;padding:6px 12px;border-radius:8px;border:1px solid #c084fc;background:linear-gradient(135deg, #F3E8FF, #EDE9FE);color:#7c3aed;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s;white-space:nowrap;min-height:38px}.btn-ai-suggest i{font-size:13px}.btn-ai-suggest:hover{background:linear-gradient(135deg, #7C3AED, #6D28D9);color:#fff;border-color:#6d28d9}.btn-ai-suggest:disabled{opacity:.7;cursor:wait}.btn-ai-suggest.btn-ai-claude{border-color:#d97706;background:linear-gradient(135deg, #FEF3C7, #FDE68A);color:#92400e}.btn-ai-suggest.btn-ai-claude:hover{background:linear-gradient(135deg, #D97706, #B45309);color:#fff;border-color:#b45309}.form_container{background:#fff;padding:25px;max-width:1300px;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.06)}.form_container.full{max-width:100%}.form_container .form_group{margin-bottom:12px;display:flex}.form_container .form_group>.label{width:300px;display:inline-flex;align-items:flex-start;justify-content:right;padding-right:12px}.form_container .form_group .input{width:calc(100% - 300px)}.default_popup{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.45);display:none;z-index:2000}.default_popup .popup_content{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);background:#fff;padding:25px;border-radius:10px;max-width:1140px;width:95%;box-shadow:0 20px 60px rgba(0,0,0,.15)}.default_popup .popup_content .popup_header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px}.default_popup .popup_content .popup_header .title{font-size:18px;font-weight:600}.default_popup .popup_content .close{cursor:pointer;color:#a0aec0;font-size:18px;padding:4px}.default_popup .popup_content .close:hover{color:#c00}.dt-layout-table{margin-bottom:20px}.pagination button{border:1px solid #e2e8f0;background:#fff;display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;margin:0 2px;border-radius:4px;transition:all .2s;cursor:pointer}.pagination button:hover{background:#f4f6f9;border-color:#6690f4}table#products a{color:inherit;text-decoration:none}table#products .table-product-title{display:flex;justify-content:space-between}table#products .edit-product-title{display:flex;height:25px;align-items:center;justify-content:center;width:25px;cursor:pointer;background:#fff;border:1px solid #cbd5e0;color:#cbd5e0;border-radius:4px}table#products .edit-product-title:hover{background:#cbd5e0;color:#fff}table#products a.custom_name{color:#57b951 !important}.product-history-page .product-history-meta{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}.product-history-page .product-history-meta span{display:inline-flex;align-items:center;padding:5px 10px;border-radius:999px;font-size:12px;font-weight:600;color:#4e5e6a;background:#eef2ff;border:1px solid #d9e2ff}.product-history-page .product-history-chart-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);padding:20px;margin-bottom:16px}.product-history-page .chart-with-form{display:flex;gap:20px;align-items:flex-start}.product-history-page .chart-area{flex:1 1 auto;min-width:0}.product-history-page .product-history-chart{min-height:360px}.product-history-page .comment-form{width:340px;flex:0 0 340px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:14px}.product-history-page .comment-form .form-group{margin-bottom:12px}.product-history-page .comment-form label{display:block;font-weight:600;margin-bottom:6px;font-size:13px;color:#52606d}.product-history-page .comment-form input[type=date],.product-history-page .comment-form textarea{width:100%;border:1px solid #e2e8f0;border-radius:6px;padding:8px 12px;font-size:14px;font-family:"Roboto",sans-serif;background:#fff}.product-history-page .comment-form input[type=date]:focus,.product-history-page .comment-form textarea:focus{outline:none;border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.product-history-page .comment-form textarea{min-height:110px;resize:vertical}.product-history-page .comment-form .btn{width:100%;justify-content:center;padding:10px 16px}.product-history-page .comment-form .btn[disabled]{opacity:.6;cursor:not-allowed}.product-history-page .products-table-wrap{overflow-x:auto}.product-history-page .products-table-wrap .table{min-width:980px}.product-history-page .products-table-wrap .comment-cell{display:flex;align-items:center;justify-content:space-between;gap:10px}.product-history-page .products-table-wrap .comment-text{word-break:break-word}.product-history-page .products-table-wrap .delete-comment{color:#c00;text-decoration:none;font-weight:600;white-space:nowrap}.product-history-page .products-table-wrap .delete-comment:hover{text-decoration:underline}.product-history-page .products-table-wrap .dt-paging .pagination .page-item{list-style:none}.cron-status-overview{display:flex;flex-wrap:wrap;gap:10px 20px;margin-bottom:20px;color:#4e5e6a;font-size:13px}.cron-progress-list{margin-bottom:20px}.cron-schedule-list{margin-bottom:20px}.cron-schedule-item{border:1px solid #dfe7f0;background:#f4f8fd;border-radius:8px;padding:9px 12px;margin-bottom:8px}.cron-schedule-item:last-child{margin-bottom:0}.cron-schedule-item strong{display:block;color:#2d3748;font-size:13px;font-weight:700;margin-bottom:2px}.cron-schedule-item small{display:block;color:#678;font-size:12px;line-height:1.35}.cron-progress-item{margin-bottom:14px}.cron-progress-item:last-child{margin-bottom:0}.cron-progress-item .cron-progress-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:6px;font-size:13px}.cron-progress-item .cron-progress-head strong{color:#2d3748;font-weight:600}.cron-progress-item .cron-progress-head span{color:#6b7a89;font-size:12px;font-weight:600;white-space:nowrap}.cron-progress-item small{display:block;margin-top:5px;color:#789;font-size:12px}.cron-progress-bar{width:100%;height:10px;border-radius:999px;background:#e9eef5;overflow:hidden}.cron-progress-bar>span{display:block;height:100%;background:linear-gradient(90deg, #5A9BFF 0%, #2E6BDF 100%)}.cron-url-list{margin-bottom:20px}.cron-url-item{border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;padding:10px 12px;margin-bottom:10px}.cron-url-item:last-child{margin-bottom:0}.cron-url-item .cron-url-top{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:6px}.cron-url-item .cron-url-top strong{color:#2d3748;font-size:13px;font-weight:600}.cron-url-item .cron-url-top small{color:#7a8794;font-size:11px;white-space:nowrap}.cron-url-item code{display:block;background:#eef2f7;border:1px solid #dde4ed;border-radius:6px;padding:6px 8px;color:#2e3b49;font-size:12px;overflow-x:auto}.cron-url-item .cron-url-plan{display:block;color:#6c7b8a;font-size:11px;margin-bottom:6px}@media(max-width: 1200px){.product-history-page .chart-with-form{flex-direction:column}.product-history-page .comment-form{width:100%;flex:1 1 auto}}.jconfirm-box .form-group .select2-container,.adspro-dialog-box .form-group .select2-container{width:100% !important;margin-top:8px}.jconfirm-box .select2-container--default .select2-selection--single,.adspro-dialog-box .select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #e2e8f0;border-radius:6px;min-height:42px;display:flex;align-items:center;padding:4px 12px;box-shadow:none;transition:border-color .2s,box-shadow .2s;font-size:14px}.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__rendered,.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__rendered{padding-left:0;line-height:1.4;color:#495057}.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__placeholder,.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__placeholder{color:#cbd5e0}.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__arrow,.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__arrow{height:100%;right:8px}.jconfirm-box .select2-container--default.select2-container--focus .select2-selection--single,.jconfirm-box .select2-container--default .select2-selection--single:hover,.adspro-dialog-box .select2-container--default.select2-container--focus .select2-selection--single,.adspro-dialog-box .select2-container--default .select2-selection--single:hover{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1);outline:0}.jconfirm-box .select2-container .select2-dropdown,.adspro-dialog-box .select2-container .select2-dropdown{border-color:#e2e8f0;border-radius:0 0 6px 6px;font-size:14px}.jconfirm-box .select2-container .select2-search--dropdown .select2-search__field,.adspro-dialog-box .select2-container .select2-search--dropdown .select2-search__field{padding:6px 10px;border-radius:4px;border:1px solid #e2e8f0;font-size:14px}.jconfirm-box .select2-container--default .select2-results__option--highlighted[aria-selected],.adspro-dialog-box .select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#6690f4;color:#fff}@media(max-width: 992px){.sidebar{transform:translateX(-100%)}.sidebar.mobile-open{transform:translateX(0)}.main-wrapper{margin-left:0 !important}}.campaign-terms-wrap{display:flex;flex-direction:column;gap:20px;margin-top:20px}.campaign-terms-page{max-width:100%;overflow:hidden}.campaign-terms-page .campaigns-filters{flex-wrap:wrap}.campaign-terms-page .campaigns-filters .filter-group{min-width:220px}.campaign-terms-page .campaigns-filters .filter-group.terms-columns-group{min-width:280px}.campaign-terms-page .terms-card-toggle{margin-left:auto;width:28px;height:28px;border:1px solid #e2e8f0;border-radius:6px;background:#fff;color:#475569;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:all .2s}.campaign-terms-page .terms-card-toggle:hover{background:#f8fafc;border-color:#cbd5e1}.campaign-terms-page .terms-adgroups-card.is-collapsed .campaigns-extra-table-wrap{display:none}.campaign-terms-page .terms-search-toolbar{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid #eef2f7;background:#fff}.campaign-terms-page .terms-search-toolbar label{font-size:12px;font-weight:600;color:#475569;display:inline-flex;align-items:center;gap:6px;margin:0;white-space:nowrap}.campaign-terms-page .terms-search-toolbar .terms-search-toolbar-label{min-width:86px}.campaign-terms-page .terms-search-toolbar #terms_min_clicks_all,.campaign-terms-page .terms-search-toolbar #terms_max_clicks_all{width:160px;height:32px}.campaign-terms-page .terms-search-toolbar #terms_min_conversions_all,.campaign-terms-page .terms-search-toolbar #terms_max_conversions_all{width:130px;max-width:130px}.campaign-terms-page .terms-search-selected-label{margin:0;font-size:12px;color:#475569;font-weight:600;white-space:nowrap}.campaign-terms-page .terms-ai-analyze-btn{margin-left:auto;display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border-radius:6px;border:1px solid #bfdbfe;background:#eff6ff;color:#1d4ed8;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s}.campaign-terms-page .terms-ai-analyze-btn:hover{background:#dbeafe;border-color:#93c5fd}.campaign-terms-page .terms-ai-analyze-btn:disabled{opacity:.6;cursor:wait}.campaign-terms-page .terms-negative-toolbar{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid #eef2f7;background:#fff}.campaign-terms-page .terms-negative-bulk-btn{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border-radius:6px;border:1px solid #fecaca;background:#fef2f2;color:#dc2626;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s}.campaign-terms-page .terms-negative-bulk-btn:hover{background:#fee2e2;border-color:#fca5a5}.campaign-terms-page .terms-negative-bulk-btn:disabled{opacity:.5;cursor:not-allowed}.campaign-terms-page table.campaigns-extra-table>thead>tr>th{position:sticky;top:0;z-index:2;background-color:#111827 !important;color:#e5e7eb !important;border-bottom:1px solid #0b1220 !important;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;padding:10px 12px;white-space:nowrap}.campaign-terms-page #terms_search_table thead th .dt-column-order,.campaign-terms-page #terms_negative_table thead th .dt-column-order{display:none !important}.campaign-terms-page #terms_search_table thead th.dt-orderable-asc,.campaign-terms-page #terms_search_table thead th.dt-orderable-desc,.campaign-terms-page #terms_negative_table thead th.dt-orderable-asc,.campaign-terms-page #terms_negative_table thead th.dt-orderable-desc{cursor:pointer;padding-right:34px;overflow:hidden}.campaign-terms-page #terms_search_table thead th .dt-column-title,.campaign-terms-page #terms_negative_table thead th .dt-column-title{display:block;overflow:hidden;text-overflow:ellipsis;padding-right:2px}.campaign-terms-page #terms_search_table thead th.dt-orderable-asc::after,.campaign-terms-page #terms_search_table thead th.dt-orderable-desc::after,.campaign-terms-page #terms_negative_table thead th.dt-orderable-asc::after,.campaign-terms-page #terms_negative_table thead th.dt-orderable-desc::after{content:"↕";position:absolute;right:10px;top:50%;transform:translateY(-50%);width:16px;height:16px;border-radius:999px;font-size:12px;font-weight:700;line-height:16px;text-align:center;color:#e5e7eb;background:#374151}.campaign-terms-page #terms_search_table thead th.dt-ordering-asc::after,.campaign-terms-page #terms_negative_table thead th.dt-ordering-asc::after,.campaign-terms-page #terms_search_table thead th[aria-sort=ascending]::after,.campaign-terms-page #terms_negative_table thead th[aria-sort=ascending]::after{content:"▲";color:#fff;background:#2563eb}.campaign-terms-page #terms_search_table thead th.dt-ordering-desc::after,.campaign-terms-page #terms_negative_table thead th.dt-ordering-desc::after,.campaign-terms-page #terms_search_table thead th[aria-sort=descending]::after,.campaign-terms-page #terms_negative_table thead th[aria-sort=descending]::after{content:"▼";color:#fff;background:#2563eb}.campaign-terms-page #terms_negative_select_all,.campaign-terms-page .terms-negative-select-row,.campaign-terms-page #terms_search_select_all,.campaign-terms-page .terms-search-select-row{width:14px;height:14px;cursor:pointer}.campaign-terms-page .dt-layout-row:first-child{display:none}.campaign-terms-page .dt-layout-row{padding:10px 12px;margin:0 !important;border-top:1px solid #f1f5f9}.campaign-terms-page .dt-info{font-size:12px;color:#64748b}.campaign-terms-page .dt-paging .pagination{margin:0;padding:0;list-style:none !important;display:flex;align-items:center;gap:6px}.campaign-terms-page .dt-paging .pagination .page-item{list-style:none !important}.campaign-terms-page .dt-paging .pagination .page-item .page-link{display:inline-flex;align-items:center;justify-content:center;min-width:36px;width:-moz-fit-content;width:fit-content;height:32px;padding:0 12px;border-radius:6px;font-size:12px;font-weight:500;border:1px solid #e2e8f0;background:#fff;color:#4e5e6a;text-decoration:none;line-height:1;white-space:nowrap}.campaign-terms-page .dt-paging .pagination .page-item .page-link:hover{background:#eef2ff;color:#6690f4;border-color:#6690f4}.campaign-terms-page .dt-paging .pagination .page-item.previous .page-link,.campaign-terms-page .dt-paging .pagination .page-item.next .page-link{min-width:72px}.campaign-terms-page .dt-paging .pagination .page-item.active .page-link{background:#6690f4;color:#fff;border-color:#6690f4}.campaign-terms-page .dt-paging .pagination .page-item.disabled .page-link{opacity:.35;cursor:default;pointer-events:none}.terms-columns-box{display:flex;flex-direction:column;gap:6px}.terms-columns-control{border:1px solid #e2e8f0;border-radius:6px;background:#fff;overflow:hidden}.terms-columns-control summary{cursor:pointer;padding:8px 10px;font-size:12px;font-weight:600;color:#334155;list-style:none}.terms-columns-control summary::-webkit-details-marker{display:none}.terms-columns-control summary::after{content:"▼";float:right;font-size:10px;color:#64748b;margin-top:2px}.terms-columns-control[open] summary::after{content:"▲"}.terms-columns-list{border-top:1px solid #eef2f7;padding:8px 10px;max-height:180px;overflow-y:auto}.terms-columns-list .terms-col-item{display:flex;align-items:center;gap:8px;font-size:12px;color:#334155;margin-bottom:6px}.terms-columns-list .terms-col-item:last-child{margin-bottom:0}.terms-columns-list .terms-col-item input[type=checkbox]{margin:0}.campaigns-extra-card{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow:hidden}.campaigns-extra-card-title{padding:14px 16px;border-bottom:1px solid #e2e8f0;font-size:13px;font-weight:700;color:#334155;display:flex;align-items:center;gap:8px}.campaigns-extra-card-title .terms-card-title-label{display:inline-flex;align-items:center;gap:8px}.campaigns-extra-table-wrap{overflow:auto}.campaigns-extra-table{margin:0;width:100%;table-layout:fixed}.campaigns-extra-table tbody td{padding:9px 12px;border-bottom:1px solid #f1f5f9;font-size:13px;color:#334155;vertical-align:middle;white-space:nowrap}.campaigns-extra-table td.num-cell{text-align:right;white-space:nowrap}.campaigns-extra-table td.text-cell{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.campaigns-extra-table th.terms-negative-select-cell,.campaigns-extra-table td.terms-negative-select-cell,.campaigns-extra-table th.terms-search-select-cell,.campaigns-extra-table td.terms-search-select-cell{text-align:center}.campaigns-extra-table th.phrase-nowrap,.campaigns-extra-table td.phrase-nowrap{white-space:nowrap !important;overflow:hidden;text-overflow:ellipsis}.campaigns-extra-table .terms-add-negative-btn,.campaigns-extra-table .terms-remove-negative-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;cursor:pointer;transition:all .2s}.campaigns-extra-table .terms-add-negative-btn{border:1px solid #e2e8f0;background:#eef2ff;color:#3b82f6}.campaigns-extra-table .terms-add-negative-btn:hover{background:#3b82f6;color:#fff;border-color:#3b82f6}.campaigns-extra-table .terms-remove-negative-btn{border:1px solid #fecaca;background:#fef2f2;color:#dc2626}.campaigns-extra-table .terms-remove-negative-btn:hover{background:#dc2626;color:#fff;border-color:#dc2626}.campaigns-extra-table tbody tr:hover{background:#f8fafc}.campaigns-extra-table tbody tr.term-is-negative td{color:#dc2626}.campaigns-extra-table tbody tr.term-is-negative:hover{background:#fef2f2}.campaigns-empty-row{text-align:center;color:#94a3b8 !important;font-style:italic}.terms-ai-modal-toolbar{display:flex;align-items:center;gap:10px;margin-bottom:10px}.terms-ai-modal-toolbar label{font-size:12px;font-weight:600;color:#334155;margin:0}.terms-ai-modal-toolbar .form-control{width:200px;height:32px}.terms-ai-summary{font-size:12px;color:#64748b;margin-bottom:10px}.terms-ai-results-wrap{border:1px solid #e2e8f0;border-radius:8px;max-height:420px;overflow:auto}.terms-ai-results-table{width:100%;border-collapse:collapse;font-size:12px}.terms-ai-results-table th,.terms-ai-results-table td{border-bottom:1px solid #eef2f7;padding:8px;vertical-align:middle}.terms-ai-results-table th{position:sticky;top:0;background:#f8fafc;color:#334155;font-weight:700}.terms-ai-results-table td.term-col{min-width:260px;max-width:380px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.terms-ai-results-table td.reason-col{min-width:320px}.terms-ai-action-badge{display:inline-flex;align-items:center;justify-content:center;border-radius:999px;padding:2px 8px;font-size:11px;font-weight:700}.terms-ai-action-badge.action-exclude{background:#fee2e2;color:#b91c1c}.terms-ai-action-badge.action-keep{background:#dcfce7;color:#166534}.products-page .products-filters .filter-group.filter-group-columns{min-width:240px}.products-columns-control{border:1px solid #e2e8f0;border-radius:6px;background:#fff;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:"▼";float:right;font-size:10px;color:#64748b;margin-top:2px}.products-columns-control[open] summary::after{content:"▲"}.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}#products th:last-child,#products td:last-child{white-space:nowrap}#products .products-row-actions{display:inline-flex;align-items:center;gap:4px}#products .products-row-actions .btn{width:38px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;border-radius:4px !important}#products .products-row-actions .btn i{line-height:1}.products-page table#products>thead>tr>th{position:sticky;top:0;z-index:2;background-color:#111827 !important;color:#e5e7eb !important;border-bottom:1px solid #0b1220 !important;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;padding:10px 12px;white-space:nowrap}.products-page #products thead th .dt-column-order{display:none !important}.products-page #products thead th.dt-orderable-asc,.products-page #products thead th.dt-orderable-desc{cursor:pointer;padding-right:34px;overflow:hidden}.products-page #products thead th .dt-column-title{display:block;overflow:hidden;text-overflow:ellipsis;padding-right:2px}.products-page #products thead th.dt-orderable-asc::after,.products-page #products thead th.dt-orderable-desc::after{content:"↕";position:absolute;right:10px;top:50%;transform:translateY(-50%);width:16px;height:16px;border-radius:999px;font-size:12px;font-weight:700;line-height:16px;text-align:center;color:#e5e7eb;background:#374151}.products-page #products thead th.dt-ordering-asc::after,.products-page #products thead th[aria-sort=ascending]::after{content:"▲";color:#fff;background:#2563eb}.products-page #products thead th.dt-ordering-desc::after,.products-page #products thead th[aria-sort=descending]::after{content:"▼";color:#fff;background:#2563eb}/*# sourceMappingURL=style.css.map */ \ No newline at end of file diff --git a/layout/style.css.map b/layout/style.css.map index e7fcf6b..76a7cb6 100644 --- a/layout/style.css.map +++ b/layout/style.css.map @@ -1 +1 @@ -{"version":3,"sources":["style.css","style.scss"],"names":[],"mappings":"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","file":"style.css","sourcesContent":["*{box-sizing:border-box}body{font-family:\"Roboto\",sans-serif;margin:0;padding:0;font-size:14px;color:#4e5e6a;background:#f4f6f9;max-width:100vw;overflow-x:hidden}.hide{display:none}small{font-size:.75em}.text-right{text-align:right}.text-bold{font-weight:700 !important}.nowrap{white-space:nowrap}body.unlogged{background:#f4f6f9;margin:0;padding:0}.login-container{display:flex;min-height:100vh}.login-brand{flex:0 0 45%;background:linear-gradient(135deg, #1E2A3A 0%, #2C3E57 50%, #6690F4 100%);display:flex;align-items:center;justify-content:center;padding:60px;position:relative;overflow:hidden}.login-brand::before{content:\"\";position:absolute;top:-50%;right:-50%;width:100%;height:100%;background:radial-gradient(circle, rgba(102, 144, 244, 0.15) 0%, transparent 70%);border-radius:50%}.login-brand .brand-content{position:relative;z-index:1;color:#fff;max-width:400px}.login-brand .brand-logo{font-size:48px;font-weight:300;margin-bottom:20px;letter-spacing:-1px}.login-brand .brand-logo strong{font-weight:700}.login-brand .brand-tagline{font-size:18px;opacity:.85;line-height:1.6;margin-bottom:50px}.login-brand .brand-features .feature{display:flex;align-items:center;gap:15px;margin-bottom:20px;opacity:.8}.login-brand .brand-features .feature i{font-size:20px;width:40px;height:40px;display:flex;align-items:center;justify-content:center;background:hsla(0,0%,100%,.1);border-radius:10px}.login-brand .brand-features .feature span{font-size:15px}.login-form-wrapper{flex:1;display:flex;align-items:center;justify-content:center;padding:60px;background:#fff}.login-box{width:100%;max-width:420px}.login-box .login-header{margin-bottom:35px}.login-box .login-header h1{font-size:28px;font-weight:700;color:#2d3748;margin:0 0 8px}.login-box .login-header p{color:#718096;font-size:15px;margin:0}.login-box .form-group{margin-bottom:20px}.login-box .form-group label{display:block;font-size:13px;font-weight:600;color:#2d3748;margin-bottom:6px}.login-box .input-with-icon{position:relative}.login-box .input-with-icon i{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#a0aec0;font-size:14px}.login-box .input-with-icon .form-control{padding-left:42px}.login-box .form-control{width:100%;height:46px;border:2px solid #e2e8f0;border-radius:8px;padding:0 14px;font-size:14px;font-family:\"Roboto\",sans-serif;color:#2d3748;transition:border-color .3s,box-shadow .3s}.login-box .form-control::placeholder{color:#cbd5e0}.login-box .form-control:focus{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.15);outline:none}.login-box .form-error{color:#c00;font-size:12px;margin-top:4px}.login-box .checkbox-group .checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:#718096;font-weight:400}.login-box .checkbox-group .checkbox-label input[type=checkbox]{width:16px;height:16px;accent-color:#6690f4}.login-box .btn-login{width:100%;height:48px;font-size:15px;font-weight:600;border-radius:8px;display:flex;align-items:center;justify-content:center;gap:8px}.login-box .btn-login.disabled{opacity:.7;pointer-events:none}.login-box .alert{display:none;padding:12px 16px;border-radius:8px;font-size:13px;margin-bottom:20px}.login-box .alert.alert-danger{background:#fff5f5;color:#c00;border:1px solid #fed7d7}.login-box .alert.alert-success{background:#f0fff4;color:#276749;border:1px solid #c6f6d5}@media(max-width: 768px){.login-brand{display:none}.login-form-wrapper{padding:30px 20px}}body.logged{display:flex;min-height:100vh;background:#f4f6f9}.sidebar{width:260px;min-height:100vh;background:#1e2a3a;position:fixed;top:0;left:0;z-index:1000;display:flex;flex-direction:column;transition:width .3s ease;overflow:hidden}.sidebar.collapsed{width:70px}.sidebar.collapsed .sidebar-header{padding:16px 0;justify-content:center}.sidebar.collapsed .sidebar-header .sidebar-logo{display:none}.sidebar.collapsed .sidebar-header .sidebar-toggle i{transform:rotate(180deg)}.sidebar.collapsed .sidebar-nav ul li a{padding:12px 0;justify-content:center}.sidebar.collapsed .sidebar-nav ul li a span{display:none}.sidebar.collapsed .sidebar-nav ul li a i{margin-right:0;font-size:18px}.sidebar.collapsed .sidebar-nav ul li.nav-group .nav-group-label{padding:12px 0;justify-content:center}.sidebar.collapsed .sidebar-nav ul li.nav-group .nav-group-label span{display:none}.sidebar.collapsed .sidebar-nav ul li.nav-group .nav-group-label i{margin-right:0;font-size:18px}.sidebar.collapsed .sidebar-footer .sidebar-user{justify-content:center}.sidebar.collapsed .sidebar-footer .sidebar-user .user-info{display:none}.sidebar.collapsed .sidebar-footer .sidebar-logout{justify-content:center}.sidebar.collapsed .sidebar-footer .sidebar-logout span{display:none}.sidebar.collapsed .nav-divider{margin:8px 15px}.sidebar-header{display:flex;align-items:center;justify-content:space-between;padding:20px 20px 16px;border-bottom:1px solid hsla(0,0%,100%,.08)}.sidebar-header .sidebar-logo a{color:#fff;text-decoration:none;font-size:24px;font-weight:300;letter-spacing:-0.5px}.sidebar-header .sidebar-logo a strong{font-weight:700}.sidebar-header .sidebar-toggle{background:none;border:none;color:#a8b7c7;cursor:pointer;padding:6px;border-radius:6px;transition:all .3s}.sidebar-header .sidebar-toggle:hover{background:hsla(0,0%,100%,.08);color:#fff}.sidebar-header .sidebar-toggle i{transition:transform .3s}.sidebar-nav{flex:1;padding:12px 0;overflow-y:auto}.sidebar-nav ul{list-style:none;margin:0;padding:0}.sidebar-nav ul li.nav-group{margin-bottom:4px}.sidebar-nav ul li.nav-group .nav-group-label{display:flex;align-items:center;padding:11px 20px;color:#d5deea;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;border-left:3px solid rgba(0,0,0,0)}.sidebar-nav ul li.nav-group .nav-group-label i{width:20px;text-align:center;margin-right:12px;font-size:14px;color:#b6c4d3}.sidebar-nav ul li.nav-group .nav-submenu{margin:0;padding:0;list-style:none}.sidebar-nav ul li.nav-group .nav-submenu li a{padding-left:44px}.sidebar-nav ul li.nav-group.active>.nav-group-label{color:#fff;background:rgba(102,144,244,.12);border-left-color:#6690f4}.sidebar-nav ul li.nav-group.active>.nav-group-label i{color:#6690f4}.sidebar-nav ul li.nav-divider{height:1px;background:hsla(0,0%,100%,.08);margin:8px 20px}.sidebar-nav ul li a{display:flex;align-items:center;padding:11px 20px;color:#a8b7c7;text-decoration:none;font-size:14px;transition:all .2s;border-left:3px solid rgba(0,0,0,0)}.sidebar-nav ul li a i{width:20px;text-align:center;margin-right:12px;font-size:15px}.sidebar-nav ul li a:hover{background:#263548;color:#fff}.sidebar-nav ul li.active>a{background:rgba(102,144,244,.15);color:#fff;border-left-color:#6690f4}.sidebar-nav ul li.active>a i{color:#6690f4}.badge-alerts-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;margin-left:8px;border-radius:50%;font-size:11px;font-weight:600;line-height:1;background:#fff;color:#6690f4}.sidebar-footer{padding:16px 20px;border-top:1px solid hsla(0,0%,100%,.08)}.sidebar-footer .sidebar-user{display:flex;align-items:center;gap:10px;margin-bottom:12px}.sidebar-footer .sidebar-user .user-avatar{width:34px;height:34px;border-radius:50%;background:rgba(102,144,244,.2);display:flex;align-items:center;justify-content:center;color:#6690f4;font-size:14px;flex-shrink:0}.sidebar-footer .sidebar-user .user-info{overflow:hidden}.sidebar-footer .sidebar-user .user-info .user-email{color:#a8b7c7;font-size:12px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sidebar-footer .sidebar-logout{display:flex;align-items:center;gap:8px;color:#e53e3e;text-decoration:none;font-size:13px;padding:8px 10px;border-radius:6px;transition:all .2s}.sidebar-footer .sidebar-logout i{font-size:14px}.sidebar-footer .sidebar-logout:hover{background:rgba(229,62,62,.1)}.main-wrapper{margin-left:260px;flex:1;min-height:100vh;transition:margin-left .3s ease;display:flex;flex-direction:column}.main-wrapper.expanded{margin-left:70px}.topbar{height:56px;background:#fff;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;padding:0 25px;position:sticky;top:0;z-index:500}.topbar .topbar-toggle{background:none;border:none;color:#4e5e6a;cursor:pointer;padding:8px 10px;border-radius:6px;font-size:16px;margin-right:15px;transition:all .2s}.topbar .topbar-toggle:hover{background:#f4f6f9}.topbar .topbar-breadcrumb{font-size:16px;font-weight:600;color:#2d3748}.content{flex:1;padding:25px}.app-alert{background:#ebf8ff;border:1px solid #bee3f8;color:#2b6cb0;padding:12px 16px;border-radius:8px;margin-bottom:20px;font-size:14px}.btn{padding:10px 20px;transition:all .2s ease;color:#fff;border:0;border-radius:6px;cursor:pointer;display:inline-flex;text-decoration:none;gap:6px;justify-content:center;align-items:center;font-size:14px;font-family:\"Roboto\",sans-serif;font-weight:500}.btn.btn_small,.btn.btn-xs,.btn.btn-sm{padding:5px 10px;font-size:12px}.btn.btn_small i,.btn.btn-xs i,.btn.btn-sm i{font-size:11px}.btn.btn-success{background:#57b951}.btn.btn-success:hover{background:#4a9c3b}.btn.btn-primary{background:#6690f4}.btn.btn-primary:hover{background:#3164db}.btn.btn-danger{background:#c00}.btn.btn-danger:hover{background:#b30000}.btn.disabled{opacity:.6;pointer-events:none}.form-control{border:1px solid #e2e8f0;border-radius:6px;height:38px;width:100%;padding:6px 12px;font-family:\"Roboto\",sans-serif;font-size:14px;color:#2d3748;transition:border-color .2s,box-shadow .2s}.form-control option{padding:5px}.form-control:focus{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1);outline:none}input[type=checkbox]{border:1px solid #e2e8f0}table{border-collapse:collapse;font-size:13px}.table{width:100%}.table th,.table td{border:1px solid #e2e8f0;padding:8px 10px}.table th{background:#f7fafc;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.03em;color:#718096}.table td.center{text-align:center}.table td.left{text-align:left}.table.table-sm td{padding:5px !important}.table input.form-control{font-size:13px;height:32px}.card{background:#fff;padding:20px;border-radius:8px;color:#2d3748;font-size:14px;box-shadow:0 1px 3px rgba(0,0,0,.06)}.card.mb25{margin-bottom:20px}.card .card-header{font-weight:600;font-size:15px}.card .card-body{padding-top:12px}.card .card-body table th,.card .card-body table td{font-size:13px}.card .card-body table th.bold,.card .card-body table td.bold{font-weight:600}.card .card-body table th.text-right,.card .card-body table td.text-right{text-align:right}.card .card-body table th.text-center,.card .card-body table td.text-center{text-align:center}.action_menu{display:flex;margin-bottom:20px;gap:12px}.action_menu .btn{padding:8px 16px}.action_menu .btn.btn_add{background:#57b951}.action_menu .btn.btn_add:hover{background:#4a9c3b}.action_menu .btn.btn_cancel{background:#c00}.action_menu .btn.btn_cancel:hover{background:#b30000}.settings-tabs{display:flex;gap:8px;margin-bottom:18px}.settings-tabs .settings-tab{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:8px;text-decoration:none;color:#6b7a89;background:#e9eef5;border:1px solid #d8e0ea;font-size:13px;font-weight:600;transition:all .2s}.settings-tabs .settings-tab:hover{color:#2d3748;background:#dde6f2}.settings-tabs .settings-tab.active{color:#fff;background:#6690f4;border-color:#6690f4}.settings-card{background:#fff;border-radius:10px;padding:28px;box-shadow:0 1px 4px rgba(0,0,0,.06)}.settings-card .settings-card-header{display:flex;align-items:center;gap:14px;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid #e2e8f0}.settings-card .settings-card-header .settings-card-icon{width:44px;height:44px;border-radius:10px;background:rgb(225.706097561,233.7475609756,252.893902439);color:#6690f4;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}.settings-card .settings-card-header h3{margin:0;font-size:17px;font-weight:600;color:#2d3748}.settings-card .settings-card-header small{color:#8899a6;font-size:13px}.settings-card .settings-field{margin-bottom:18px}.settings-card .settings-field label{display:block;font-size:13px;font-weight:600;color:#2d3748;margin-bottom:6px}.settings-card .settings-input-wrap{position:relative}.settings-card .settings-input-wrap .settings-input-icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#a0aec0;font-size:14px;pointer-events:none}.settings-card .settings-input-wrap .form-control{padding-left:38px}.settings-card .settings-input-wrap .settings-toggle-pw{position:absolute;right:4px;top:50%;transform:translateY(-50%);background:none;border:none;color:#a0aec0;cursor:pointer;padding:6px 10px;font-size:14px;transition:color .2s}.settings-card .settings-input-wrap .settings-toggle-pw:hover{color:#6690f4}.settings-card .settings-field .settings-toggle-label{display:inline-flex;align-items:center;gap:10px;cursor:pointer;font-size:14px;font-weight:500;user-select:none;margin-bottom:0;width:100%}.settings-card .settings-field .settings-toggle-label .settings-toggle-text{flex:1 1 auto;min-width:0;line-height:1.35}.settings-card .settings-toggle-checkbox{display:none}.settings-card .settings-toggle-checkbox+.settings-toggle-switch{display:inline-block;position:relative;width:44px;height:24px;background:#ccc;border-radius:12px;transition:background .2s;flex-shrink:0}.settings-card .settings-toggle-checkbox+.settings-toggle-switch::after{content:\"\";position:absolute;top:3px;left:3px;width:18px;height:18px;background:#fff;border-radius:50%;transition:transform .2s}.settings-card .settings-toggle-checkbox:checked+.settings-toggle-switch{background:#22c55e}.settings-card .settings-toggle-checkbox:checked+.settings-toggle-switch::after{transform:translateX(20px)}.settings-card .settings-fields-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 24px}@media(max-width: 768px){.settings-card .settings-fields-grid{grid-template-columns:1fr}}.settings-card .settings-alert-error{display:flex;align-items:center;gap:10px;background:#fff5f5;color:#c00;border:1px solid #fed7d7;border-radius:8px;padding:12px 16px;margin-bottom:20px;font-size:13px}.settings-card .settings-alert-error i{font-size:16px;flex-shrink:0}.clients-page .clients-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.clients-page .clients-header h2{margin:0;font-size:20px;font-weight:600;color:#2d3748}.clients-page .clients-header h2 i{color:#6690f4;margin-right:8px}.clients-page .clients-table-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow:hidden}.clients-page .clients-table-wrap .table{margin:0}.clients-page .clients-table-wrap .table thead th{background:#f8fafc;border-bottom:2px solid #e2e8f0;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;padding:14px 20px}.clients-page .clients-table-wrap .table tbody td{padding:14px 20px;vertical-align:middle;border-bottom:1px solid #f1f5f9}.clients-page .clients-table-wrap .table tbody tr:hover{background:#f8fafc}.clients-page .clients-table-wrap .table .client-id{color:#8899a6;font-size:13px;font-weight:600}.clients-page .clients-table-wrap .table .client-name{font-weight:600;color:#2d3748}.clients-page .badge-id{display:inline-block;background:#eef2ff;color:#6690f4;font-size:13px;font-weight:600;padding:4px 10px;border-radius:6px;font-family:monospace}.clients-page .actions-cell{text-align:center;white-space:nowrap}.clients-page .btn-icon{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:8px;border:none;cursor:pointer;font-size:14px;transition:all .2s;margin:0 2px}.clients-page .btn-icon.btn-icon-edit{background:#eef2ff;color:#6690f4}.clients-page .btn-icon.btn-icon-edit:hover{background:#6690f4;color:#fff}.clients-page .btn-icon.btn-icon-delete{background:#fff5f5;color:#c00}.clients-page .btn-icon.btn-icon-delete:hover{background:#c00;color:#fff}.clients-page .btn-icon.btn-icon-sync{background:#f0fdf4;color:#16a34a}.clients-page .btn-icon.btn-icon-sync:hover{background:#16a34a;color:#fff}.clients-page .btn-icon.btn-icon-sync:disabled{opacity:.7;cursor:wait}.clients-page .btn-icon.btn-icon-sync.is-queued{background:#fef3c7;color:#d97706}.clients-page .client-sync-bars{display:flex;flex-direction:column;gap:4px}.clients-page .client-sync-row{display:flex;align-items:center;gap:4px}.clients-page .client-sync-label{font-size:11px;font-weight:600;color:#8899a6;width:18px;flex-shrink:0}.clients-page .client-sync-track{flex:1;height:6px;border-radius:999px;background:#e9eef5;overflow:hidden}.clients-page .client-sync-fill{height:100%;border-radius:999px;background:#cbd5e0;transition:width .4s ease}.clients-page .client-sync-fill.is-active{background:linear-gradient(90deg, #5A9BFF 0%, #2E6BDF 100%)}.clients-page .client-sync-fill.is-done{background:#57b951}.clients-page .client-sync-pct{font-size:11px;font-weight:600;color:#8899a6;width:32px;text-align:right;flex-shrink:0}.clients-page .empty-state{text-align:center;padding:50px 20px !important;color:#a0aec0}.clients-page .empty-state i{font-size:40px;margin-bottom:12px;display:block}.clients-page .empty-state p{margin:0;font-size:15px}.btn-secondary{background:#e2e8f0;color:#2d3748;border:none;padding:8px 18px;border-radius:6px;font-size:14px;cursor:pointer;transition:background .2s}.btn-secondary:hover{background:#cbd5e0}.campaigns-page{max-width:100%;overflow-x:hidden;width:100%}.campaigns-page .campaigns-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.campaigns-page .campaigns-header h2{margin:0;font-size:20px;font-weight:600;color:#2d3748}.campaigns-page .campaigns-header h2 i{color:#6690f4;margin-right:8px}.campaigns-page .campaigns-filters{display:flex;flex-wrap:wrap;gap:20px;margin-bottom:20px}.campaigns-page .campaigns-filters .filter-group{flex:1;min-width:0}.campaigns-page .campaigns-filters .filter-group label{display:block;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;margin-bottom:6px}.campaigns-page .campaigns-filters .filter-group label i{margin-right:4px}.campaigns-page .campaigns-filters .filter-group .form-control{width:100%;padding:10px 14px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;color:#2d3748;background:#fff;transition:border-color .2s;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}.campaigns-page .campaigns-filters .filter-group .form-control:focus{outline:none;border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.campaigns-page .campaigns-filters .filter-group .filter-with-action{display:flex;align-items:center;gap:8px}.campaigns-page .campaigns-filters .filter-group .filter-with-action .form-control{flex:1}.campaigns-page .campaigns-filters .filter-group .filter-with-action .btn-icon{flex-shrink:0;width:42px;height:42px;display:inline-flex;align-items:center;justify-content:center;border-radius:8px;border:none;cursor:pointer;font-size:14px;transition:all .2s}.campaigns-page .campaigns-filters .filter-group .filter-with-action .btn-icon.btn-icon-delete{background:#fff5f5;color:#c00}.campaigns-page .campaigns-filters .filter-group .filter-with-action .btn-icon.btn-icon-delete:hover{background:#c00;color:#fff}.campaigns-page .campaigns-filters .filter-group-campaign-multi{flex:2 !important}.campaigns-page .campaigns-filters .campaign-dropdown{flex:1;min-width:0;position:relative}.campaigns-page .campaigns-filters .campaign-dropdown-trigger{width:100%;padding:10px 14px;padding-right:32px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;color:#2d3748;background:#fff;cursor:pointer;display:flex;align-items:center;transition:border-color .2s;position:relative;min-height:42px;box-sizing:border-box}.campaigns-page .campaigns-filters .campaign-dropdown-trigger .campaign-dropdown-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.campaigns-page .campaigns-filters .campaign-dropdown-trigger .campaign-dropdown-text.is-placeholder{color:#8899a6}.campaigns-page .campaigns-filters .campaign-dropdown-trigger .campaign-dropdown-arrow{position:absolute;right:12px;font-size:10px;color:#8899a6;transition:transform .2s}.campaigns-page .campaigns-filters .campaign-dropdown.is-open .campaign-dropdown-trigger{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.campaigns-page .campaigns-filters .campaign-dropdown.is-open .campaign-dropdown-arrow{transform:rotate(180deg)}.campaigns-page .campaigns-filters .campaign-dropdown.is-open .campaign-dropdown-menu{display:block}.campaigns-page .campaigns-filters .campaign-dropdown-menu{display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:100;max-height:280px;overflow-y:auto;background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.1);padding:4px 0}.campaigns-page .campaigns-filters .campaign-dropdown-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:14px;color:#2d3748;margin:0;transition:background .15s}.campaigns-page .campaigns-filters .campaign-dropdown-item:hover{background:#f8fafc}.campaigns-page .campaigns-filters .campaign-dropdown-item.is-checked{background:#eef2ff}.campaigns-page .campaigns-filters .campaign-dropdown-item input[type=checkbox]{width:16px;height:16px;cursor:pointer;flex-shrink:0;accent-color:#6690f4}.campaigns-page .campaigns-filters .campaign-dropdown-item span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.campaigns-page .campaigns-list-panel{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);margin-bottom:20px;overflow:hidden}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #e2e8f0;gap:12px}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left{display:flex;align-items:center;gap:8px;font-size:13px;color:#4e5e6a}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left input[type=checkbox]{width:16px;height:16px;cursor:pointer}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left label{cursor:pointer;user-select:none;margin:0}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left .campaigns-selected-count{margin-left:12px;color:#8899a6}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-right .campaigns-bulk-delete-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;background:#fff5f5;color:#c00;transition:all .2s}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-right .campaigns-bulk-delete-btn:hover:not(:disabled){background:#c00;color:#fff}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-right .campaigns-bulk-delete-btn:disabled{opacity:.4;cursor:not-allowed}.campaigns-page .campaigns-list-panel .campaigns-list-items{display:flex;flex-wrap:wrap;gap:0;padding:8px 8px;max-height:220px;overflow-y:auto}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item{display:flex;align-items:center;gap:8px;padding:6px 12px;margin:2px;border-radius:6px;font-size:13px;color:#2d3748;cursor:pointer;user-select:none;transition:background .15s}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item:hover{background:#f0f4ff}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item input[type=checkbox]{width:15px;height:15px;cursor:pointer;flex-shrink:0}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item .campaigns-list-item-name{white-space:nowrap}.campaigns-page .campaigns-chart-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);padding:20px;margin-bottom:20px;min-height:350px;overflow:hidden}.campaigns-page .campaigns-chart-wrap #container{max-width:100%}.campaigns-page .campaigns-table-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow-x:auto;-ms-overflow-style:none;scrollbar-width:none;max-width:100%}.campaigns-page .campaigns-table-wrap::-webkit-scrollbar{display:none}.campaigns-page .campaigns-table-wrap .table{margin:0;width:100% !important}.campaigns-page .campaigns-table-wrap .table thead th{background:#f8fafc;border-bottom:2px solid #e2e8f0;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;padding:12px 16px;white-space:nowrap}.campaigns-page .campaigns-table-wrap .table tbody td{padding:10px 16px;vertical-align:middle;border-bottom:1px solid #f1f5f9;font-size:13px}.campaigns-page .campaigns-table-wrap .table tbody tr:hover{background:#f8fafc}.campaigns-page .campaigns-table-wrap .dt-layout-row{padding:14px 20px;margin:0 !important;border-top:1px solid #f1f5f9}.campaigns-page .campaigns-table-wrap .dt-layout-row:first-child{display:none}.campaigns-page .campaigns-table-wrap .dt-info{font-size:13px;color:#8899a6}.campaigns-page .campaigns-table-wrap .dt-paging .pagination{margin:0;padding:0;list-style:none;display:flex;align-items:center;gap:6px}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .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 #e2e8f0;background:#fff;color:#4e5e6a;cursor:pointer;transition:all .2s;text-decoration:none;line-height:1;white-space:nowrap}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item .page-link:hover{background:#eef2ff;color:#6690f4;border-color:#6690f4}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item.active .page-link{background:#6690f4;color:#fff;border-color:#6690f4;font-weight:600}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item.disabled .page-link{opacity:.35;cursor:default;pointer-events:none}.campaigns-page .campaigns-table-wrap .dt-processing{background:hsla(0,0%,100%,.9);color:#4e5e6a;font-size:14px}.campaigns-page .delete-history-entry{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:6px;border:none;cursor:pointer;font-size:12px;background:#fff5f5;color:#c00;transition:all .2s}.campaigns-page .delete-history-entry:hover{background:#c00;color:#fff}.products-page .products-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.products-page .products-header h2{margin:0;font-size:20px;font-weight:600;color:#2d3748}.products-page .products-header h2 i{color:#6690f4;margin-right:8px}.products-page .products-filters{display:flex;flex-wrap:wrap;align-items:flex-end;gap:20px;margin-bottom:16px}.products-page .products-filters .filter-group{flex:1 1 220px;min-width:0}.products-page .products-filters .filter-group label{display:block;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;margin-bottom:6px}.products-page .products-filters .filter-group label i{margin-right:4px}.products-page .products-filters .filter-group .form-control{width:100%;padding:10px 14px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;color:#2d3748;background:#fff;transition:border-color .2s}.products-page .products-filters .filter-group .form-control:focus{outline:none;border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.products-page .products-filters .filter-group 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}.products-page .products-filters .filter-group.filter-group-client,.products-page .products-filters .filter-group.filter-group-campaign,.products-page .products-filters .filter-group.filter-group-ad-group{flex:1 1 260px}.products-page .products-filters .filter-group.filter-group-roas{flex:0 0 200px}.products-page .products-filters .filter-group.filter-group-columns{flex:0 0 240px}.products-page .products-actions{margin-bottom:12px}.products-page .products-actions .btn-danger{padding:7px 14px;font-size:13px;border-radius:6px;border:none;cursor:pointer;transition:all .2s}.products-page .products-actions .btn-danger:disabled{opacity:.4;cursor:default}.products-page .products-table-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow:hidden}.products-page .products-table-wrap .table{margin:0;width:100% !important}.products-page .products-table-wrap .table thead th{background:#f8fafc;border-bottom:2px solid #e2e8f0;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:#8899a6;padding:10px 8px;white-space:nowrap}.products-page .products-table-wrap .table tbody td{padding:6px 8px;vertical-align:middle;border-bottom:1px solid #f1f5f9;font-size:12px}.products-page .products-table-wrap .table tbody tr:hover{background:#f8fafc}.products-page .products-table-wrap .table input.min_roas,.products-page .products-table-wrap .table input.form-control-sm,.products-page .products-table-wrap .table select.custom_label_4,.products-page .products-table-wrap .table select.form-control-sm{padding:3px 6px;font-size:12px;border:1px solid #e2e8f0;border-radius:4px;background:#fff}.products-page .products-table-wrap .dt-layout-row{padding:14px 20px;margin:0 !important;border-top:1px solid #f1f5f9}.products-page .products-table-wrap .dt-layout-row:first-child{display:none}.products-page .products-table-wrap .dt-info{font-size:13px;color:#8899a6}.products-page .products-table-wrap .dt-paging .pagination{margin:0;padding:0;list-style:none;display:flex;align-items:center;gap:6px}.products-page .products-table-wrap .dt-paging .pagination .page-item .page-link{display:inline-flex;align-items:center;justify-content:center;min-width:36px;height:36px;padding:0 14px;border-radius:8px;font-size:13px;font-weight:500;border:1px solid #e2e8f0;background:#fff;color:#4e5e6a;cursor:pointer;transition:all .2s;text-decoration:none;line-height:1;white-space:nowrap}.products-page .products-table-wrap .dt-paging .pagination .page-item .page-link:hover{background:#eef2ff;color:#6690f4;border-color:#6690f4}.products-page .products-table-wrap .dt-paging .pagination .page-item.active .page-link{background:#6690f4;color:#fff;border-color:#6690f4;font-weight:600}.products-page .products-table-wrap .dt-paging .pagination .page-item.disabled .page-link{opacity:.35;cursor:default;pointer-events:none}.products-page .products-table-wrap .dt-processing{background:hsla(0,0%,100%,.9);color:#4e5e6a;font-size:14px}.products-page .delete-product{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;border:none;cursor:pointer;font-size:12px;background:#fff5f5;color:#c00;transition:all .2s}.products-page .delete-product:hover{background:#c00;color:#fff}.products-page .edit-product-title{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;border:none;cursor:pointer;font-size:12px;background:#eef2ff;color:#6690f4;transition:all .2s}.products-page .edit-product-title:hover{background:#6690f4;color:#fff}.desc-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}.desc-header label{margin:0}.desc-tabs{display:flex;gap:2px;background:#eee;border-radius:6px;padding:2px}.desc-tab{border:none;background:rgba(0,0,0,0);padding:4px 12px;font-size:12px;border-radius:4px;cursor:pointer;color:#666;transition:all .15s ease}.desc-tab i{margin-right:4px}.desc-tab.active{background:#fff;color:#333;box-shadow:0 1px 3px rgba(0,0,0,.12);font-weight:500}.desc-tab:hover:not(.active){color:#333}.desc-wrap{flex:1;min-width:0}.desc-preview ul,.desc-preview ol{margin:6px 0;padding-left:20px}.desc-preview li{margin-bottom:3px}.desc-preview b,.desc-preview strong{font-weight:600}.input-with-ai{display:flex;gap:8px;align-items:flex-start}.input-with-ai .form-control{flex:1}.btn-ai-suggest{display:inline-flex;align-items:center;gap:4px;padding:6px 12px;border-radius:8px;border:1px solid #c084fc;background:linear-gradient(135deg, #F3E8FF, #EDE9FE);color:#7c3aed;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s;white-space:nowrap;min-height:38px}.btn-ai-suggest i{font-size:13px}.btn-ai-suggest:hover{background:linear-gradient(135deg, #7C3AED, #6D28D9);color:#fff;border-color:#6d28d9}.btn-ai-suggest:disabled{opacity:.7;cursor:wait}.btn-ai-suggest.btn-ai-claude{border-color:#d97706;background:linear-gradient(135deg, #FEF3C7, #FDE68A);color:#92400e}.btn-ai-suggest.btn-ai-claude:hover{background:linear-gradient(135deg, #D97706, #B45309);color:#fff;border-color:#b45309}.form_container{background:#fff;padding:25px;max-width:1300px;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.06)}.form_container.full{max-width:100%}.form_container .form_group{margin-bottom:12px;display:flex}.form_container .form_group>.label{width:300px;display:inline-flex;align-items:flex-start;justify-content:right;padding-right:12px}.form_container .form_group .input{width:calc(100% - 300px)}.default_popup{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.45);display:none;z-index:2000}.default_popup .popup_content{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);background:#fff;padding:25px;border-radius:10px;max-width:1140px;width:95%;box-shadow:0 20px 60px rgba(0,0,0,.15)}.default_popup .popup_content .popup_header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px}.default_popup .popup_content .popup_header .title{font-size:18px;font-weight:600}.default_popup .popup_content .close{cursor:pointer;color:#a0aec0;font-size:18px;padding:4px}.default_popup .popup_content .close:hover{color:#c00}.dt-layout-table{margin-bottom:20px}.pagination button{border:1px solid #e2e8f0;background:#fff;display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;margin:0 2px;border-radius:4px;transition:all .2s;cursor:pointer}.pagination button:hover{background:#f4f6f9;border-color:#6690f4}table#products a{color:inherit;text-decoration:none}table#products .table-product-title{display:flex;justify-content:space-between}table#products .edit-product-title{display:flex;height:25px;align-items:center;justify-content:center;width:25px;cursor:pointer;background:#fff;border:1px solid #cbd5e0;color:#cbd5e0;border-radius:4px}table#products .edit-product-title:hover{background:#cbd5e0;color:#fff}table#products a.custom_name{color:#57b951 !important}.product-history-page .product-history-meta{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}.product-history-page .product-history-meta span{display:inline-flex;align-items:center;padding:5px 10px;border-radius:999px;font-size:12px;font-weight:600;color:#4e5e6a;background:#eef2ff;border:1px solid #d9e2ff}.product-history-page .product-history-chart-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);padding:20px;margin-bottom:16px}.product-history-page .chart-with-form{display:flex;gap:20px;align-items:flex-start}.product-history-page .chart-area{flex:1 1 auto;min-width:0}.product-history-page .product-history-chart{min-height:360px}.product-history-page .comment-form{width:340px;flex:0 0 340px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:14px}.product-history-page .comment-form .form-group{margin-bottom:12px}.product-history-page .comment-form label{display:block;font-weight:600;margin-bottom:6px;font-size:13px;color:#52606d}.product-history-page .comment-form input[type=date],.product-history-page .comment-form textarea{width:100%;border:1px solid #e2e8f0;border-radius:6px;padding:8px 12px;font-size:14px;font-family:\"Roboto\",sans-serif;background:#fff}.product-history-page .comment-form input[type=date]:focus,.product-history-page .comment-form textarea:focus{outline:none;border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.product-history-page .comment-form textarea{min-height:110px;resize:vertical}.product-history-page .comment-form .btn{width:100%;justify-content:center;padding:10px 16px}.product-history-page .comment-form .btn[disabled]{opacity:.6;cursor:not-allowed}.product-history-page .products-table-wrap{overflow-x:auto}.product-history-page .products-table-wrap .table{min-width:980px}.product-history-page .products-table-wrap .comment-cell{display:flex;align-items:center;justify-content:space-between;gap:10px}.product-history-page .products-table-wrap .comment-text{word-break:break-word}.product-history-page .products-table-wrap .delete-comment{color:#c00;text-decoration:none;font-weight:600;white-space:nowrap}.product-history-page .products-table-wrap .delete-comment:hover{text-decoration:underline}.product-history-page .products-table-wrap .dt-paging .pagination .page-item{list-style:none}.cron-status-overview{display:flex;flex-wrap:wrap;gap:10px 20px;margin-bottom:20px;color:#4e5e6a;font-size:13px}.cron-progress-list{margin-bottom:20px}.cron-progress-item{margin-bottom:14px}.cron-progress-item:last-child{margin-bottom:0}.cron-progress-item .cron-progress-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:6px;font-size:13px}.cron-progress-item .cron-progress-head strong{color:#2d3748;font-weight:600}.cron-progress-item .cron-progress-head span{color:#6b7a89;font-size:12px;font-weight:600;white-space:nowrap}.cron-progress-item small{display:block;margin-top:5px;color:#789;font-size:12px}.cron-progress-bar{width:100%;height:10px;border-radius:999px;background:#e9eef5;overflow:hidden}.cron-progress-bar>span{display:block;height:100%;background:linear-gradient(90deg, #5A9BFF 0%, #2E6BDF 100%)}.cron-url-list{margin-bottom:20px}.cron-url-item{border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;padding:10px 12px;margin-bottom:10px}.cron-url-item:last-child{margin-bottom:0}.cron-url-item .cron-url-top{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:6px}.cron-url-item .cron-url-top strong{color:#2d3748;font-size:13px;font-weight:600}.cron-url-item .cron-url-top small{color:#7a8794;font-size:11px;white-space:nowrap}.cron-url-item code{display:block;background:#eef2f7;border:1px solid #dde4ed;border-radius:6px;padding:6px 8px;color:#2e3b49;font-size:12px;overflow-x:auto}@media(max-width: 1200px){.product-history-page .chart-with-form{flex-direction:column}.product-history-page .comment-form{width:100%;flex:1 1 auto}}.jconfirm-box .form-group .select2-container,.adspro-dialog-box .form-group .select2-container{width:100% !important;margin-top:8px}.jconfirm-box .select2-container--default .select2-selection--single,.adspro-dialog-box .select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #e2e8f0;border-radius:6px;min-height:42px;display:flex;align-items:center;padding:4px 12px;box-shadow:none;transition:border-color .2s,box-shadow .2s;font-size:14px}.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__rendered,.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__rendered{padding-left:0;line-height:1.4;color:#495057}.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__placeholder,.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__placeholder{color:#cbd5e0}.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__arrow,.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__arrow{height:100%;right:8px}.jconfirm-box .select2-container--default.select2-container--focus .select2-selection--single,.jconfirm-box .select2-container--default .select2-selection--single:hover,.adspro-dialog-box .select2-container--default.select2-container--focus .select2-selection--single,.adspro-dialog-box .select2-container--default .select2-selection--single:hover{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1);outline:0}.jconfirm-box .select2-container .select2-dropdown,.adspro-dialog-box .select2-container .select2-dropdown{border-color:#e2e8f0;border-radius:0 0 6px 6px;font-size:14px}.jconfirm-box .select2-container .select2-search--dropdown .select2-search__field,.adspro-dialog-box .select2-container .select2-search--dropdown .select2-search__field{padding:6px 10px;border-radius:4px;border:1px solid #e2e8f0;font-size:14px}.jconfirm-box .select2-container--default .select2-results__option--highlighted[aria-selected],.adspro-dialog-box .select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#6690f4;color:#fff}@media(max-width: 992px){.sidebar{transform:translateX(-100%)}.sidebar.mobile-open{transform:translateX(0)}.main-wrapper{margin-left:0 !important}}.campaign-terms-wrap{display:flex;flex-direction:column;gap:20px;margin-top:20px}.campaign-terms-page{max-width:100%;overflow:hidden}.campaign-terms-page .campaigns-filters{flex-wrap:wrap}.campaign-terms-page .campaigns-filters .filter-group{min-width:220px}.campaign-terms-page .campaigns-filters .filter-group.terms-columns-group{min-width:280px}.campaign-terms-page .terms-card-toggle{margin-left:auto;width:28px;height:28px;border:1px solid #e2e8f0;border-radius:6px;background:#fff;color:#475569;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:all .2s}.campaign-terms-page .terms-card-toggle:hover{background:#f8fafc;border-color:#cbd5e1}.campaign-terms-page .terms-adgroups-card.is-collapsed .campaigns-extra-table-wrap{display:none}.campaign-terms-page .terms-search-toolbar{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid #eef2f7;background:#fff}.campaign-terms-page .terms-search-toolbar label{font-size:12px;font-weight:600;color:#475569;display:inline-flex;align-items:center;gap:6px;margin:0;white-space:nowrap}.campaign-terms-page .terms-search-toolbar .terms-search-toolbar-label{min-width:86px}.campaign-terms-page .terms-search-toolbar #terms_min_clicks_all,.campaign-terms-page .terms-search-toolbar #terms_max_clicks_all{width:160px;height:32px}.campaign-terms-page .terms-search-toolbar #terms_min_conversions_all,.campaign-terms-page .terms-search-toolbar #terms_max_conversions_all{width:130px;max-width:130px}.campaign-terms-page .terms-search-selected-label{margin:0;font-size:12px;color:#475569;font-weight:600;white-space:nowrap}.campaign-terms-page .terms-ai-analyze-btn{margin-left:auto;display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border-radius:6px;border:1px solid #bfdbfe;background:#eff6ff;color:#1d4ed8;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s}.campaign-terms-page .terms-ai-analyze-btn:hover{background:#dbeafe;border-color:#93c5fd}.campaign-terms-page .terms-ai-analyze-btn:disabled{opacity:.6;cursor:wait}.campaign-terms-page .terms-negative-toolbar{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid #eef2f7;background:#fff}.campaign-terms-page .terms-negative-bulk-btn{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border-radius:6px;border:1px solid #fecaca;background:#fef2f2;color:#dc2626;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s}.campaign-terms-page .terms-negative-bulk-btn:hover{background:#fee2e2;border-color:#fca5a5}.campaign-terms-page .terms-negative-bulk-btn:disabled{opacity:.5;cursor:not-allowed}.campaign-terms-page table.campaigns-extra-table>thead>tr>th{position:sticky;top:0;z-index:2;background-color:#111827 !important;color:#e5e7eb !important;border-bottom:1px solid #0b1220 !important;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;padding:10px 12px;white-space:nowrap}.campaign-terms-page #terms_search_table thead th .dt-column-order,.campaign-terms-page #terms_negative_table thead th .dt-column-order{display:none !important}.campaign-terms-page #terms_search_table thead th.dt-orderable-asc,.campaign-terms-page #terms_search_table thead th.dt-orderable-desc,.campaign-terms-page #terms_negative_table thead th.dt-orderable-asc,.campaign-terms-page #terms_negative_table thead th.dt-orderable-desc{cursor:pointer;padding-right:34px;overflow:hidden}.campaign-terms-page #terms_search_table thead th .dt-column-title,.campaign-terms-page #terms_negative_table thead th .dt-column-title{display:block;overflow:hidden;text-overflow:ellipsis;padding-right:2px}.campaign-terms-page #terms_search_table thead th.dt-orderable-asc::after,.campaign-terms-page #terms_search_table thead th.dt-orderable-desc::after,.campaign-terms-page #terms_negative_table thead th.dt-orderable-asc::after,.campaign-terms-page #terms_negative_table thead th.dt-orderable-desc::after{content:\"↕\";position:absolute;right:10px;top:50%;transform:translateY(-50%);width:16px;height:16px;border-radius:999px;font-size:12px;font-weight:700;line-height:16px;text-align:center;color:#e5e7eb;background:#374151}.campaign-terms-page #terms_search_table thead th.dt-ordering-asc::after,.campaign-terms-page #terms_negative_table thead th.dt-ordering-asc::after,.campaign-terms-page #terms_search_table thead th[aria-sort=ascending]::after,.campaign-terms-page #terms_negative_table thead th[aria-sort=ascending]::after{content:\"▲\";color:#fff;background:#2563eb}.campaign-terms-page #terms_search_table thead th.dt-ordering-desc::after,.campaign-terms-page #terms_negative_table thead th.dt-ordering-desc::after,.campaign-terms-page #terms_search_table thead th[aria-sort=descending]::after,.campaign-terms-page #terms_negative_table thead th[aria-sort=descending]::after{content:\"▼\";color:#fff;background:#2563eb}.campaign-terms-page #terms_negative_select_all,.campaign-terms-page .terms-negative-select-row,.campaign-terms-page #terms_search_select_all,.campaign-terms-page .terms-search-select-row{width:14px;height:14px;cursor:pointer}.campaign-terms-page .dt-layout-row:first-child{display:none}.campaign-terms-page .dt-layout-row{padding:10px 12px;margin:0 !important;border-top:1px solid #f1f5f9}.campaign-terms-page .dt-info{font-size:12px;color:#64748b}.campaign-terms-page .dt-paging .pagination{margin:0;padding:0;list-style:none !important;display:flex;align-items:center;gap:6px}.campaign-terms-page .dt-paging .pagination .page-item{list-style:none !important}.campaign-terms-page .dt-paging .pagination .page-item .page-link{display:inline-flex;align-items:center;justify-content:center;min-width:36px;width:fit-content;height:32px;padding:0 12px;border-radius:6px;font-size:12px;font-weight:500;border:1px solid #e2e8f0;background:#fff;color:#4e5e6a;text-decoration:none;line-height:1;white-space:nowrap}.campaign-terms-page .dt-paging .pagination .page-item .page-link:hover{background:#eef2ff;color:#6690f4;border-color:#6690f4}.campaign-terms-page .dt-paging .pagination .page-item.previous .page-link,.campaign-terms-page .dt-paging .pagination .page-item.next .page-link{min-width:72px}.campaign-terms-page .dt-paging .pagination .page-item.active .page-link{background:#6690f4;color:#fff;border-color:#6690f4}.campaign-terms-page .dt-paging .pagination .page-item.disabled .page-link{opacity:.35;cursor:default;pointer-events:none}.terms-columns-box{display:flex;flex-direction:column;gap:6px}.terms-columns-control{border:1px solid #e2e8f0;border-radius:6px;background:#fff;overflow:hidden}.terms-columns-control summary{cursor:pointer;padding:8px 10px;font-size:12px;font-weight:600;color:#334155;list-style:none}.terms-columns-control summary::-webkit-details-marker{display:none}.terms-columns-control summary::after{content:\"▼\";float:right;font-size:10px;color:#64748b;margin-top:2px}.terms-columns-control[open] summary::after{content:\"▲\"}.terms-columns-list{border-top:1px solid #eef2f7;padding:8px 10px;max-height:180px;overflow-y:auto}.terms-columns-list .terms-col-item{display:flex;align-items:center;gap:8px;font-size:12px;color:#334155;margin-bottom:6px}.terms-columns-list .terms-col-item:last-child{margin-bottom:0}.terms-columns-list .terms-col-item input[type=checkbox]{margin:0}.campaigns-extra-card{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow:hidden}.campaigns-extra-card-title{padding:14px 16px;border-bottom:1px solid #e2e8f0;font-size:13px;font-weight:700;color:#334155;display:flex;align-items:center;gap:8px}.campaigns-extra-card-title .terms-card-title-label{display:inline-flex;align-items:center;gap:8px}.campaigns-extra-table-wrap{overflow:auto}.campaigns-extra-table{margin:0;width:100%;table-layout:fixed}.campaigns-extra-table tbody td{padding:9px 12px;border-bottom:1px solid #f1f5f9;font-size:13px;color:#334155;vertical-align:middle;white-space:nowrap}.campaigns-extra-table td.num-cell{text-align:right;white-space:nowrap}.campaigns-extra-table td.text-cell{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.campaigns-extra-table th.terms-negative-select-cell,.campaigns-extra-table td.terms-negative-select-cell,.campaigns-extra-table th.terms-search-select-cell,.campaigns-extra-table td.terms-search-select-cell{text-align:center}.campaigns-extra-table th.phrase-nowrap,.campaigns-extra-table td.phrase-nowrap{white-space:nowrap !important;overflow:hidden;text-overflow:ellipsis}.campaigns-extra-table .terms-add-negative-btn,.campaigns-extra-table .terms-remove-negative-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;cursor:pointer;transition:all .2s}.campaigns-extra-table .terms-add-negative-btn{border:1px solid #e2e8f0;background:#eef2ff;color:#3b82f6}.campaigns-extra-table .terms-add-negative-btn:hover{background:#3b82f6;color:#fff;border-color:#3b82f6}.campaigns-extra-table .terms-remove-negative-btn{border:1px solid #fecaca;background:#fef2f2;color:#dc2626}.campaigns-extra-table .terms-remove-negative-btn:hover{background:#dc2626;color:#fff;border-color:#dc2626}.campaigns-extra-table tbody tr:hover{background:#f8fafc}.campaigns-extra-table tbody tr.term-is-negative td{color:#dc2626}.campaigns-extra-table tbody tr.term-is-negative:hover{background:#fef2f2}.campaigns-empty-row{text-align:center;color:#94a3b8 !important;font-style:italic}.terms-ai-modal-toolbar{display:flex;align-items:center;gap:10px;margin-bottom:10px}.terms-ai-modal-toolbar label{font-size:12px;font-weight:600;color:#334155;margin:0}.terms-ai-modal-toolbar .form-control{width:200px;height:32px}.terms-ai-summary{font-size:12px;color:#64748b;margin-bottom:10px}.terms-ai-results-wrap{border:1px solid #e2e8f0;border-radius:8px;max-height:420px;overflow:auto}.terms-ai-results-table{width:100%;border-collapse:collapse;font-size:12px}.terms-ai-results-table th,.terms-ai-results-table td{border-bottom:1px solid #eef2f7;padding:8px;vertical-align:middle}.terms-ai-results-table th{position:sticky;top:0;background:#f8fafc;color:#334155;font-weight:700}.terms-ai-results-table td.term-col{min-width:260px;max-width:380px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.terms-ai-results-table td.reason-col{min-width:320px}.terms-ai-action-badge{display:inline-flex;align-items:center;justify-content:center;border-radius:999px;padding:2px 8px;font-size:11px;font-weight:700}.terms-ai-action-badge.action-exclude{background:#fee2e2;color:#b91c1c}.terms-ai-action-badge.action-keep{background:#dcfce7;color:#166534}.products-page .products-filters .filter-group.filter-group-columns{min-width:240px}.products-columns-control{border:1px solid #e2e8f0;border-radius:6px;background:#fff;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:\"▼\";float:right;font-size:10px;color:#64748b;margin-top:2px}.products-columns-control[open] summary::after{content:\"▲\"}.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}#products th:last-child,#products td:last-child{white-space:nowrap}#products .products-row-actions{display:inline-flex;align-items:center;gap:4px}#products .products-row-actions .btn{width:38px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;border-radius:4px !important}#products .products-row-actions .btn i{line-height:1}.products-page table#products>thead>tr>th{position:sticky;top:0;z-index:2;background-color:#111827 !important;color:#e5e7eb !important;border-bottom:1px solid #0b1220 !important;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;padding:10px 12px;white-space:nowrap}.products-page #products thead th .dt-column-order{display:none !important}.products-page #products thead th.dt-orderable-asc,.products-page #products thead th.dt-orderable-desc{cursor:pointer;padding-right:34px;overflow:hidden}.products-page #products thead th .dt-column-title{display:block;overflow:hidden;text-overflow:ellipsis;padding-right:2px}.products-page #products thead th.dt-orderable-asc::after,.products-page #products thead th.dt-orderable-desc::after{content:\"↕\";position:absolute;right:10px;top:50%;transform:translateY(-50%);width:16px;height:16px;border-radius:999px;font-size:12px;font-weight:700;line-height:16px;text-align:center;color:#e5e7eb;background:#374151}.products-page #products thead th.dt-ordering-asc::after,.products-page #products thead th[aria-sort=ascending]::after{content:\"▲\";color:#fff;background:#2563eb}.products-page #products thead th.dt-ordering-desc::after,.products-page #products thead th[aria-sort=descending]::after{content:\"▼\";color:#fff;background:#2563eb}","@use \"sass:color\";\r\n// === adsPRO - Nowe style ===\r\n\r\n// --- Zmienne ---\r\n$cPrimary: #6690F4;\r\n$cPrimaryDark: #3164db;\r\n$cSidebarBg: #1E2A3A;\r\n$cSidebarText: #A8B7C7;\r\n$cSidebarHover: #263548;\r\n$cSidebarActive: $cPrimary;\r\n$cContentBg: #F4F6F9;\r\n$cWhite: #FFFFFF;\r\n$cText: #4E5E6A;\r\n$cTextDark: #2D3748;\r\n$cBorder: #E2E8F0;\r\n$cSuccess: #57B951;\r\n$cSuccessDark: #4a9c3b;\r\n$cDanger: #CC0000;\r\n$cDangerDark: #b30000;\r\n$cWarning: #FF8C00;\r\n$cGreenLight: #57b951;\r\n\r\n$sidebarWidth: 260px;\r\n$sidebarCollapsed: 70px;\r\n$topbarHeight: 56px;\r\n$transitionSpeed: 0.3s;\r\n\r\n// --- Reset i baza ---\r\n* {\r\n box-sizing: border-box;\r\n}\r\n\r\nbody {\r\n font-family: \"Roboto\", sans-serif;\r\n margin: 0;\r\n padding: 0;\r\n font-size: 14px;\r\n color: $cText;\r\n background: $cContentBg;\r\n max-width: 100vw;\r\n overflow-x: hidden;\r\n}\r\n\r\n.hide {\r\n display: none;\r\n}\r\n\r\n// --- Typografia ---\r\nsmall {\r\n font-size: .75em;\r\n}\r\n\r\n.text-right {\r\n text-align: right;\r\n}\r\n\r\n.text-bold {\r\n font-weight: 700 !important;\r\n}\r\n\r\n.nowrap {\r\n white-space: nowrap;\r\n}\r\n\r\n// ===========================\r\n// LOGIN PAGE (unlogged)\r\n// ===========================\r\nbody.unlogged {\r\n background: $cContentBg;\r\n margin: 0;\r\n padding: 0;\r\n}\r\n\r\n.login-container {\r\n display: flex;\r\n min-height: 100vh;\r\n}\r\n\r\n.login-brand {\r\n flex: 0 0 45%;\r\n background: linear-gradient(135deg, $cSidebarBg 0%, #2C3E57 50%, $cPrimary 100%);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 60px;\r\n position: relative;\r\n overflow: hidden;\r\n\r\n &::before {\r\n content: '';\r\n position: absolute;\r\n top: -50%;\r\n right: -50%;\r\n width: 100%;\r\n height: 100%;\r\n background: radial-gradient(circle, rgba($cPrimary, 0.15) 0%, transparent 70%);\r\n border-radius: 50%;\r\n }\r\n\r\n .brand-content {\r\n position: relative;\r\n z-index: 1;\r\n color: $cWhite;\r\n max-width: 400px;\r\n }\r\n\r\n .brand-logo {\r\n font-size: 48px;\r\n font-weight: 300;\r\n margin-bottom: 20px;\r\n letter-spacing: -1px;\r\n\r\n strong {\r\n font-weight: 700;\r\n }\r\n }\r\n\r\n .brand-tagline {\r\n font-size: 18px;\r\n opacity: 0.85;\r\n line-height: 1.6;\r\n margin-bottom: 50px;\r\n }\r\n\r\n .brand-features {\r\n .feature {\r\n display: flex;\r\n align-items: center;\r\n gap: 15px;\r\n margin-bottom: 20px;\r\n opacity: 0.8;\r\n\r\n i {\r\n font-size: 20px;\r\n width: 40px;\r\n height: 40px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n background: rgba($cWhite, 0.1);\r\n border-radius: 10px;\r\n }\r\n\r\n span {\r\n font-size: 15px;\r\n }\r\n }\r\n }\r\n}\r\n\r\n.login-form-wrapper {\r\n flex: 1;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 60px;\r\n background: $cWhite;\r\n}\r\n\r\n.login-box {\r\n width: 100%;\r\n max-width: 420px;\r\n\r\n .login-header {\r\n margin-bottom: 35px;\r\n\r\n h1 {\r\n font-size: 28px;\r\n font-weight: 700;\r\n color: $cTextDark;\r\n margin: 0 0 8px;\r\n }\r\n\r\n p {\r\n color: #718096;\r\n font-size: 15px;\r\n margin: 0;\r\n }\r\n }\r\n\r\n .form-group {\r\n margin-bottom: 20px;\r\n\r\n label {\r\n display: block;\r\n font-size: 13px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n margin-bottom: 6px;\r\n }\r\n }\r\n\r\n .input-with-icon {\r\n position: relative;\r\n\r\n i {\r\n position: absolute;\r\n left: 14px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n color: #A0AEC0;\r\n font-size: 14px;\r\n }\r\n\r\n .form-control {\r\n padding-left: 42px;\r\n }\r\n }\r\n\r\n .form-control {\r\n width: 100%;\r\n height: 46px;\r\n border: 2px solid $cBorder;\r\n border-radius: 8px;\r\n padding: 0 14px;\r\n font-size: 14px;\r\n font-family: \"Roboto\", sans-serif;\r\n color: $cTextDark;\r\n transition: border-color $transitionSpeed, box-shadow $transitionSpeed;\r\n\r\n &::placeholder {\r\n color: #CBD5E0;\r\n }\r\n\r\n &:focus {\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.15);\r\n outline: none;\r\n }\r\n }\r\n\r\n .form-error {\r\n color: $cDanger;\r\n font-size: 12px;\r\n margin-top: 4px;\r\n }\r\n\r\n .checkbox-group {\r\n .checkbox-label {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n cursor: pointer;\r\n font-size: 13px;\r\n color: #718096;\r\n font-weight: 400;\r\n\r\n input[type=\"checkbox\"] {\r\n width: 16px;\r\n height: 16px;\r\n accent-color: $cPrimary;\r\n }\r\n }\r\n }\r\n\r\n .btn-login {\r\n width: 100%;\r\n height: 48px;\r\n font-size: 15px;\r\n font-weight: 600;\r\n border-radius: 8px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 8px;\r\n\r\n &.disabled {\r\n opacity: 0.7;\r\n pointer-events: none;\r\n }\r\n }\r\n\r\n .alert {\r\n display: none;\r\n padding: 12px 16px;\r\n border-radius: 8px;\r\n font-size: 13px;\r\n margin-bottom: 20px;\r\n\r\n &.alert-danger {\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n border: 1px solid #FED7D7;\r\n }\r\n\r\n &.alert-success {\r\n background: #F0FFF4;\r\n color: #276749;\r\n border: 1px solid #C6F6D5;\r\n }\r\n }\r\n}\r\n\r\n// Responsywność logowania\r\n@media (max-width: 768px) {\r\n .login-brand {\r\n display: none;\r\n }\r\n\r\n .login-form-wrapper {\r\n padding: 30px 20px;\r\n }\r\n}\r\n\r\n// ===========================\r\n// LAYOUT (logged) - SIDEBAR\r\n// ===========================\r\nbody.logged {\r\n display: flex;\r\n min-height: 100vh;\r\n background: $cContentBg;\r\n}\r\n\r\n// --- Sidebar ---\r\n.sidebar {\r\n width: $sidebarWidth;\r\n min-height: 100vh;\r\n background: $cSidebarBg;\r\n position: fixed;\r\n top: 0;\r\n left: 0;\r\n z-index: 1000;\r\n display: flex;\r\n flex-direction: column;\r\n transition: width $transitionSpeed ease;\r\n overflow: hidden;\r\n\r\n &.collapsed {\r\n width: $sidebarCollapsed;\r\n\r\n .sidebar-header {\r\n padding: 16px 0;\r\n justify-content: center;\r\n\r\n .sidebar-logo {\r\n display: none;\r\n }\r\n\r\n .sidebar-toggle i {\r\n transform: rotate(180deg);\r\n }\r\n }\r\n\r\n .sidebar-nav ul li a {\r\n padding: 12px 0;\r\n justify-content: center;\r\n\r\n span {\r\n display: none;\r\n }\r\n\r\n i {\r\n margin-right: 0;\r\n font-size: 18px;\r\n }\r\n }\r\n\r\n .sidebar-nav ul li.nav-group .nav-group-label {\r\n padding: 12px 0;\r\n justify-content: center;\r\n\r\n span {\r\n display: none;\r\n }\r\n\r\n i {\r\n margin-right: 0;\r\n font-size: 18px;\r\n }\r\n }\r\n\r\n .sidebar-footer {\r\n .sidebar-user {\r\n justify-content: center;\r\n\r\n .user-info {\r\n display: none;\r\n }\r\n }\r\n\r\n .sidebar-logout {\r\n justify-content: center;\r\n\r\n span {\r\n display: none;\r\n }\r\n }\r\n }\r\n\r\n .nav-divider {\r\n margin: 8px 15px;\r\n }\r\n }\r\n}\r\n\r\n.sidebar-header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 20px 20px 16px;\r\n border-bottom: 1px solid rgba($cWhite, 0.08);\r\n\r\n .sidebar-logo a {\r\n color: $cWhite;\r\n text-decoration: none;\r\n font-size: 24px;\r\n font-weight: 300;\r\n letter-spacing: -0.5px;\r\n\r\n strong {\r\n font-weight: 700;\r\n }\r\n }\r\n\r\n .sidebar-toggle {\r\n background: none;\r\n border: none;\r\n color: $cSidebarText;\r\n cursor: pointer;\r\n padding: 6px;\r\n border-radius: 6px;\r\n transition: all $transitionSpeed;\r\n\r\n &:hover {\r\n background: rgba($cWhite, 0.08);\r\n color: $cWhite;\r\n }\r\n\r\n i {\r\n transition: transform $transitionSpeed;\r\n }\r\n }\r\n}\r\n\r\n.sidebar-nav {\r\n flex: 1;\r\n padding: 12px 0;\r\n overflow-y: auto;\r\n\r\n ul {\r\n list-style: none;\r\n margin: 0;\r\n padding: 0;\r\n\r\n li {\r\n &.nav-group {\r\n margin-bottom: 4px;\r\n\r\n .nav-group-label {\r\n display: flex;\r\n align-items: center;\r\n padding: 11px 20px;\r\n color: #D5DEEA;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.6px;\r\n border-left: 3px solid transparent;\r\n\r\n i {\r\n width: 20px;\r\n text-align: center;\r\n margin-right: 12px;\r\n font-size: 14px;\r\n color: #B6C4D3;\r\n }\r\n }\r\n\r\n .nav-submenu {\r\n margin: 0;\r\n padding: 0;\r\n list-style: none;\r\n\r\n li a {\r\n padding-left: 44px;\r\n }\r\n }\r\n\r\n &.active>.nav-group-label {\r\n color: $cWhite;\r\n background: rgba($cPrimary, 0.12);\r\n border-left-color: $cPrimary;\r\n\r\n i {\r\n color: $cPrimary;\r\n }\r\n }\r\n }\r\n\r\n &.nav-divider {\r\n height: 1px;\r\n background: rgba($cWhite, 0.08);\r\n margin: 8px 20px;\r\n }\r\n\r\n a {\r\n display: flex;\r\n align-items: center;\r\n padding: 11px 20px;\r\n color: $cSidebarText;\r\n text-decoration: none;\r\n font-size: 14px;\r\n transition: all 0.2s;\r\n border-left: 3px solid transparent;\r\n\r\n i {\r\n width: 20px;\r\n text-align: center;\r\n margin-right: 12px;\r\n font-size: 15px;\r\n }\r\n\r\n &:hover {\r\n background: $cSidebarHover;\r\n color: $cWhite;\r\n }\r\n }\r\n\r\n &.active>a {\r\n background: rgba($cPrimary, 0.15);\r\n color: $cWhite;\r\n border-left-color: $cPrimary;\r\n\r\n i {\r\n color: $cPrimary;\r\n }\r\n }\r\n }\r\n }\r\n}\r\n\r\n.badge-alerts-count {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n min-width: 20px;\r\n height: 20px;\r\n padding: 0 6px;\r\n margin-left: 8px;\r\n border-radius: 50%;\r\n font-size: 11px;\r\n font-weight: 600;\r\n line-height: 1;\r\n background: $cWhite;\r\n color: $cPrimary;\r\n}\r\n\r\n.sidebar-footer {\r\n padding: 16px 20px;\r\n border-top: 1px solid rgba($cWhite, 0.08);\r\n\r\n .sidebar-user {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n margin-bottom: 12px;\r\n\r\n .user-avatar {\r\n width: 34px;\r\n height: 34px;\r\n border-radius: 50%;\r\n background: rgba($cPrimary, 0.2);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: $cPrimary;\r\n font-size: 14px;\r\n flex-shrink: 0;\r\n }\r\n\r\n .user-info {\r\n overflow: hidden;\r\n\r\n .user-email {\r\n color: $cSidebarText;\r\n font-size: 12px;\r\n display: block;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n }\r\n }\r\n }\r\n\r\n .sidebar-logout {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n color: #E53E3E;\r\n text-decoration: none;\r\n font-size: 13px;\r\n padding: 8px 10px;\r\n border-radius: 6px;\r\n transition: all 0.2s;\r\n\r\n i {\r\n font-size: 14px;\r\n }\r\n\r\n &:hover {\r\n background: rgba(#E53E3E, 0.1);\r\n }\r\n }\r\n}\r\n\r\n// --- Main wrapper ---\r\n.main-wrapper {\r\n margin-left: $sidebarWidth;\r\n flex: 1;\r\n min-height: 100vh;\r\n transition: margin-left $transitionSpeed ease;\r\n display: flex;\r\n flex-direction: column;\r\n\r\n &.expanded {\r\n margin-left: $sidebarCollapsed;\r\n }\r\n}\r\n\r\n// --- Topbar ---\r\n.topbar {\r\n height: $topbarHeight;\r\n background: $cWhite;\r\n border-bottom: 1px solid $cBorder;\r\n display: flex;\r\n align-items: center;\r\n padding: 0 25px;\r\n position: sticky;\r\n top: 0;\r\n z-index: 500;\r\n\r\n .topbar-toggle {\r\n background: none;\r\n border: none;\r\n color: $cText;\r\n cursor: pointer;\r\n padding: 8px 10px;\r\n border-radius: 6px;\r\n font-size: 16px;\r\n margin-right: 15px;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: $cContentBg;\r\n }\r\n }\r\n\r\n .topbar-breadcrumb {\r\n font-size: 16px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n }\r\n}\r\n\r\n// --- Content area ---\r\n.content {\r\n flex: 1;\r\n padding: 25px;\r\n}\r\n\r\n.app-alert {\r\n background: #EBF8FF;\r\n border: 1px solid #BEE3F8;\r\n color: #2B6CB0;\r\n padding: 12px 16px;\r\n border-radius: 8px;\r\n margin-bottom: 20px;\r\n font-size: 14px;\r\n}\r\n\r\n// ===========================\r\n// KOMPONENTY WSPÓLNE\r\n// ===========================\r\n\r\n// --- Buttons ---\r\n.btn {\r\n padding: 10px 20px;\r\n transition: all 0.2s ease;\r\n color: $cWhite;\r\n border: 0;\r\n border-radius: 6px;\r\n cursor: pointer;\r\n display: inline-flex;\r\n text-decoration: none;\r\n gap: 6px;\r\n justify-content: center;\r\n align-items: center;\r\n font-size: 14px;\r\n font-family: \"Roboto\", sans-serif;\r\n font-weight: 500;\r\n\r\n &.btn_small,\r\n &.btn-xs,\r\n &.btn-sm {\r\n padding: 5px 10px;\r\n font-size: 12px;\r\n\r\n i {\r\n font-size: 11px;\r\n }\r\n }\r\n\r\n &.btn-success {\r\n background: $cSuccess;\r\n\r\n &:hover {\r\n background: $cSuccessDark;\r\n }\r\n }\r\n\r\n &.btn-primary {\r\n background: $cPrimary;\r\n\r\n &:hover {\r\n background: $cPrimaryDark;\r\n }\r\n }\r\n\r\n &.btn-danger {\r\n background: $cDanger;\r\n\r\n &:hover {\r\n background: $cDangerDark;\r\n }\r\n }\r\n\r\n &.disabled {\r\n opacity: 0.6;\r\n pointer-events: none;\r\n }\r\n}\r\n\r\n// --- Form controls ---\r\n.form-control {\r\n border: 1px solid $cBorder;\r\n border-radius: 6px;\r\n height: 38px;\r\n width: 100%;\r\n padding: 6px 12px;\r\n font-family: \"Roboto\", sans-serif;\r\n font-size: 14px;\r\n color: $cTextDark;\r\n transition: border-color 0.2s, box-shadow 0.2s;\r\n\r\n option {\r\n padding: 5px;\r\n }\r\n\r\n &:focus {\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);\r\n outline: none;\r\n }\r\n}\r\n\r\ninput[type=\"checkbox\"] {\r\n border: 1px solid $cBorder;\r\n}\r\n\r\n// --- Tables ---\r\ntable {\r\n border-collapse: collapse;\r\n font-size: 13px;\r\n}\r\n\r\n.table {\r\n width: 100%;\r\n\r\n th,\r\n td {\r\n border: 1px solid $cBorder;\r\n padding: 8px 10px;\r\n }\r\n\r\n th {\r\n background: #F7FAFC;\r\n font-weight: 600;\r\n font-size: 12px;\r\n text-transform: uppercase;\r\n letter-spacing: 0.03em;\r\n color: #718096;\r\n }\r\n\r\n td.center {\r\n text-align: center;\r\n }\r\n\r\n td.left {\r\n text-align: left;\r\n }\r\n\r\n &.table-sm td {\r\n padding: 5px !important;\r\n }\r\n\r\n input.form-control {\r\n font-size: 13px;\r\n height: 32px;\r\n }\r\n}\r\n\r\n// --- Cards ---\r\n.card {\r\n background: $cWhite;\r\n padding: 20px;\r\n border-radius: 8px;\r\n color: $cTextDark;\r\n font-size: 14px;\r\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);\r\n\r\n &.mb25 {\r\n margin-bottom: 20px;\r\n }\r\n\r\n .card-header {\r\n font-weight: 600;\r\n font-size: 15px;\r\n }\r\n\r\n .card-body {\r\n padding-top: 12px;\r\n\r\n table {\r\n\r\n th,\r\n td {\r\n font-size: 13px;\r\n\r\n &.bold {\r\n font-weight: 600;\r\n }\r\n\r\n &.text-right {\r\n text-align: right;\r\n }\r\n\r\n &.text-center {\r\n text-align: center;\r\n }\r\n }\r\n }\r\n }\r\n}\r\n\r\n// --- Action menu ---\r\n.action_menu {\r\n display: flex;\r\n margin-bottom: 20px;\r\n gap: 12px;\r\n\r\n .btn {\r\n padding: 8px 16px;\r\n\r\n &.btn_add {\r\n background: $cSuccess;\r\n\r\n &:hover {\r\n background: $cSuccessDark;\r\n }\r\n }\r\n\r\n &.btn_cancel {\r\n background: $cDanger;\r\n\r\n &:hover {\r\n background: $cDangerDark;\r\n }\r\n }\r\n }\r\n}\r\n\r\n// --- Settings page ---\r\n.settings-tabs {\r\n display: flex;\r\n gap: 8px;\r\n margin-bottom: 18px;\r\n\r\n .settings-tab {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n padding: 8px 14px;\r\n border-radius: 8px;\r\n text-decoration: none;\r\n color: #6B7A89;\r\n background: #E9EEF5;\r\n border: 1px solid #D8E0EA;\r\n font-size: 13px;\r\n font-weight: 600;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n color: $cTextDark;\r\n background: #DDE6F2;\r\n }\r\n\r\n &.active {\r\n color: $cWhite;\r\n background: $cPrimary;\r\n border-color: $cPrimary;\r\n }\r\n }\r\n}\r\n\r\n.settings-card {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n padding: 28px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n\r\n .settings-card-header {\r\n display: flex;\r\n align-items: center;\r\n gap: 14px;\r\n margin-bottom: 24px;\r\n padding-bottom: 16px;\r\n border-bottom: 1px solid $cBorder;\r\n\r\n .settings-card-icon {\r\n width: 44px;\r\n height: 44px;\r\n border-radius: 10px;\r\n background: color.adjust($cPrimary, $lightness: 26%);\r\n color: $cPrimary;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 18px;\r\n flex-shrink: 0;\r\n }\r\n\r\n h3 {\r\n margin: 0;\r\n font-size: 17px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n }\r\n\r\n small {\r\n color: #8899A6;\r\n font-size: 13px;\r\n }\r\n }\r\n\r\n .settings-field {\r\n margin-bottom: 18px;\r\n\r\n label {\r\n display: block;\r\n font-size: 13px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n margin-bottom: 6px;\r\n }\r\n }\r\n\r\n .settings-input-wrap {\r\n position: relative;\r\n\r\n .settings-input-icon {\r\n position: absolute;\r\n left: 12px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n color: #A0AEC0;\r\n font-size: 14px;\r\n pointer-events: none;\r\n }\r\n\r\n .form-control {\r\n padding-left: 38px;\r\n }\r\n\r\n .settings-toggle-pw {\r\n position: absolute;\r\n right: 4px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n background: none;\r\n border: none;\r\n color: #A0AEC0;\r\n cursor: pointer;\r\n padding: 6px 10px;\r\n font-size: 14px;\r\n transition: color 0.2s;\r\n\r\n &:hover {\r\n color: $cPrimary;\r\n }\r\n }\r\n }\r\n\r\n .settings-field .settings-toggle-label {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 10px;\r\n cursor: pointer;\r\n font-size: 14px;\r\n font-weight: 500;\r\n user-select: none;\r\n margin-bottom: 0;\r\n width: 100%;\r\n\r\n .settings-toggle-text {\r\n flex: 1 1 auto;\r\n min-width: 0;\r\n line-height: 1.35;\r\n }\r\n }\r\n\r\n .settings-toggle-checkbox {\r\n display: none;\r\n\r\n &+.settings-toggle-switch {\r\n display: inline-block;\r\n position: relative;\r\n width: 44px;\r\n height: 24px;\r\n background: #ccc;\r\n border-radius: 12px;\r\n transition: background 0.2s;\r\n flex-shrink: 0;\r\n\r\n &::after {\r\n content: '';\r\n position: absolute;\r\n top: 3px;\r\n left: 3px;\r\n width: 18px;\r\n height: 18px;\r\n background: #fff;\r\n border-radius: 50%;\r\n transition: transform 0.2s;\r\n }\r\n }\r\n\r\n &:checked+.settings-toggle-switch {\r\n background: #22C55E;\r\n\r\n &::after {\r\n transform: translateX(20px);\r\n }\r\n }\r\n }\r\n\r\n .settings-fields-grid {\r\n display: grid;\r\n grid-template-columns: 1fr 1fr;\r\n gap: 0 24px;\r\n\r\n @media (max-width: 768px) {\r\n grid-template-columns: 1fr;\r\n }\r\n }\r\n\r\n .settings-alert-error {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n border: 1px solid #FED7D7;\r\n border-radius: 8px;\r\n padding: 12px 16px;\r\n margin-bottom: 20px;\r\n font-size: 13px;\r\n\r\n i {\r\n font-size: 16px;\r\n flex-shrink: 0;\r\n }\r\n }\r\n}\r\n\r\n// --- Clients page ---\r\n.clients-page {\r\n .clients-header {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 20px;\r\n\r\n h2 {\r\n margin: 0;\r\n font-size: 20px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n\r\n i {\r\n color: $cPrimary;\r\n margin-right: 8px;\r\n }\r\n }\r\n }\r\n\r\n .clients-table-wrap {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n overflow: hidden;\r\n\r\n .table {\r\n margin: 0;\r\n\r\n thead th {\r\n background: #F8FAFC;\r\n border-bottom: 2px solid $cBorder;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.5px;\r\n color: #8899A6;\r\n padding: 14px 20px;\r\n }\r\n\r\n tbody td {\r\n padding: 14px 20px;\r\n vertical-align: middle;\r\n border-bottom: 1px solid #F1F5F9;\r\n }\r\n\r\n tbody tr:hover {\r\n background: #F8FAFC;\r\n }\r\n\r\n .client-id {\r\n color: #8899A6;\r\n font-size: 13px;\r\n font-weight: 600;\r\n }\r\n\r\n .client-name {\r\n font-weight: 600;\r\n color: $cTextDark;\r\n }\r\n }\r\n }\r\n\r\n .badge-id {\r\n display: inline-block;\r\n background: #EEF2FF;\r\n color: $cPrimary;\r\n font-size: 13px;\r\n font-weight: 600;\r\n padding: 4px 10px;\r\n border-radius: 6px;\r\n font-family: monospace;\r\n }\r\n\r\n .actions-cell {\r\n text-align: center;\r\n white-space: nowrap;\r\n }\r\n\r\n .btn-icon {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 34px;\r\n height: 34px;\r\n border-radius: 8px;\r\n border: none;\r\n cursor: pointer;\r\n font-size: 14px;\r\n transition: all 0.2s;\r\n margin: 0 2px;\r\n\r\n &.btn-icon-edit {\r\n background: #EEF2FF;\r\n color: $cPrimary;\r\n\r\n &:hover {\r\n background: $cPrimary;\r\n color: $cWhite;\r\n }\r\n }\r\n\r\n &.btn-icon-delete {\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n\r\n &:hover {\r\n background: $cDanger;\r\n color: $cWhite;\r\n }\r\n }\r\n\r\n &.btn-icon-sync {\r\n background: #F0FDF4;\r\n color: #16a34a;\r\n\r\n &:hover {\r\n background: #16a34a;\r\n color: $cWhite;\r\n }\r\n\r\n &:disabled {\r\n opacity: 0.7;\r\n cursor: wait;\r\n }\r\n\r\n &.is-queued {\r\n background: #FEF3C7;\r\n color: #D97706;\r\n }\r\n }\r\n }\r\n\r\n .client-sync-bars {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 4px;\r\n }\r\n\r\n .client-sync-row {\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n }\r\n\r\n .client-sync-label {\r\n font-size: 11px;\r\n font-weight: 600;\r\n color: #8899A6;\r\n width: 18px;\r\n flex-shrink: 0;\r\n }\r\n\r\n .client-sync-track {\r\n flex: 1;\r\n height: 6px;\r\n border-radius: 999px;\r\n background: #E9EEF5;\r\n overflow: hidden;\r\n }\r\n\r\n .client-sync-fill {\r\n height: 100%;\r\n border-radius: 999px;\r\n background: #CBD5E0;\r\n transition: width 0.4s ease;\r\n\r\n &.is-active {\r\n background: linear-gradient(90deg, #5A9BFF 0%, #2E6BDF 100%);\r\n }\r\n\r\n &.is-done {\r\n background: $cSuccess;\r\n }\r\n }\r\n\r\n .client-sync-pct {\r\n font-size: 11px;\r\n font-weight: 600;\r\n color: #8899A6;\r\n width: 32px;\r\n text-align: right;\r\n flex-shrink: 0;\r\n }\r\n\r\n .empty-state {\r\n text-align: center;\r\n padding: 50px 20px !important;\r\n color: #A0AEC0;\r\n\r\n i {\r\n font-size: 40px;\r\n margin-bottom: 12px;\r\n display: block;\r\n }\r\n\r\n p {\r\n margin: 0;\r\n font-size: 15px;\r\n }\r\n }\r\n}\r\n\r\n.btn-secondary {\r\n background: #E2E8F0;\r\n color: $cTextDark;\r\n border: none;\r\n padding: 8px 18px;\r\n border-radius: 6px;\r\n font-size: 14px;\r\n cursor: pointer;\r\n transition: background 0.2s;\r\n\r\n &:hover {\r\n background: #CBD5E0;\r\n }\r\n}\r\n\r\n// ===========================\r\n// CAMPAIGNS PAGE\r\n// ===========================\r\n.campaigns-page {\r\n max-width: 100%;\r\n overflow-x: hidden;\r\n width: 100%;\r\n\r\n .campaigns-header {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 20px;\r\n\r\n h2 {\r\n margin: 0;\r\n font-size: 20px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n\r\n i {\r\n color: $cPrimary;\r\n margin-right: 8px;\r\n }\r\n }\r\n }\r\n\r\n .campaigns-filters {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 20px;\r\n margin-bottom: 20px;\r\n\r\n .filter-group {\r\n flex: 1;\r\n min-width: 0;\r\n\r\n label {\r\n display: block;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.5px;\r\n color: #8899A6;\r\n margin-bottom: 6px;\r\n\r\n i {\r\n margin-right: 4px;\r\n }\r\n }\r\n\r\n .form-control {\r\n width: 100%;\r\n padding: 10px 14px;\r\n border: 1px solid $cBorder;\r\n border-radius: 8px;\r\n font-size: 14px;\r\n color: $cTextDark;\r\n background: $cWhite;\r\n transition: border-color 0.2s;\r\n appearance: none;\r\n -webkit-appearance: none;\r\n 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\");\r\n background-repeat: no-repeat;\r\n background-position: right 12px center;\r\n padding-right: 32px;\r\n\r\n &:focus {\r\n outline: none;\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);\r\n }\r\n }\r\n\r\n .filter-with-action {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n\r\n .form-control {\r\n flex: 1;\r\n }\r\n\r\n .btn-icon {\r\n flex-shrink: 0;\r\n width: 42px;\r\n height: 42px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border-radius: 8px;\r\n border: none;\r\n cursor: pointer;\r\n font-size: 14px;\r\n transition: all 0.2s;\r\n\r\n &.btn-icon-delete {\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n\r\n &:hover {\r\n background: $cDanger;\r\n color: $cWhite;\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n .filter-group-campaign-multi {\r\n flex: 2 !important;\r\n }\r\n\r\n .campaign-dropdown {\r\n flex: 1;\r\n min-width: 0;\r\n position: relative;\r\n }\r\n\r\n .campaign-dropdown-trigger {\r\n width: 100%;\r\n padding: 10px 14px;\r\n padding-right: 32px;\r\n border: 1px solid $cBorder;\r\n border-radius: 8px;\r\n font-size: 14px;\r\n color: $cTextDark;\r\n background: $cWhite;\r\n cursor: pointer;\r\n display: flex;\r\n align-items: center;\r\n transition: border-color 0.2s;\r\n position: relative;\r\n min-height: 42px;\r\n box-sizing: border-box;\r\n\r\n .campaign-dropdown-text {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n\r\n &.is-placeholder {\r\n color: #8899A6;\r\n }\r\n }\r\n\r\n .campaign-dropdown-arrow {\r\n position: absolute;\r\n right: 12px;\r\n font-size: 10px;\r\n color: #8899A6;\r\n transition: transform 0.2s;\r\n }\r\n }\r\n\r\n .campaign-dropdown.is-open {\r\n .campaign-dropdown-trigger {\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);\r\n }\r\n\r\n .campaign-dropdown-arrow {\r\n transform: rotate(180deg);\r\n }\r\n\r\n .campaign-dropdown-menu {\r\n display: block;\r\n }\r\n }\r\n\r\n .campaign-dropdown-menu {\r\n display: none;\r\n position: absolute;\r\n top: calc(100% + 4px);\r\n left: 0;\r\n right: 0;\r\n z-index: 100;\r\n max-height: 280px;\r\n overflow-y: auto;\r\n background: $cWhite;\r\n border: 1px solid $cBorder;\r\n border-radius: 8px;\r\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\r\n padding: 4px 0;\r\n }\r\n\r\n .campaign-dropdown-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 8px 12px;\r\n cursor: pointer;\r\n font-size: 14px;\r\n color: $cTextDark;\r\n margin: 0;\r\n transition: background 0.15s;\r\n\r\n &:hover {\r\n background: #F8FAFC;\r\n }\r\n\r\n &.is-checked {\r\n background: #EEF2FF;\r\n }\r\n\r\n input[type=\"checkbox\"] {\r\n width: 16px;\r\n height: 16px;\r\n cursor: pointer;\r\n flex-shrink: 0;\r\n accent-color: $cPrimary;\r\n }\r\n\r\n span {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n }\r\n }\r\n }\r\n\r\n .campaigns-list-panel {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n margin-bottom: 20px;\r\n overflow: hidden;\r\n\r\n .campaigns-list-toolbar {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 12px 16px;\r\n border-bottom: 1px solid $cBorder;\r\n gap: 12px;\r\n\r\n &-left {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n font-size: 13px;\r\n color: $cText;\r\n\r\n input[type=\"checkbox\"] {\r\n width: 16px;\r\n height: 16px;\r\n cursor: pointer;\r\n }\r\n\r\n label {\r\n cursor: pointer;\r\n user-select: none;\r\n margin: 0;\r\n }\r\n\r\n .campaigns-selected-count {\r\n margin-left: 12px;\r\n color: #8899A6;\r\n }\r\n }\r\n\r\n &-right {\r\n .campaigns-bulk-delete-btn {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n padding: 8px 16px;\r\n border: none;\r\n border-radius: 8px;\r\n font-size: 13px;\r\n font-weight: 600;\r\n cursor: pointer;\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n transition: all 0.2s;\r\n\r\n &:hover:not(:disabled) {\r\n background: $cDanger;\r\n color: $cWhite;\r\n }\r\n\r\n &:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n }\r\n }\r\n }\r\n }\r\n\r\n .campaigns-list-items {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 0;\r\n padding: 8px 8px;\r\n max-height: 220px;\r\n overflow-y: auto;\r\n\r\n .campaigns-list-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 6px 12px;\r\n margin: 2px;\r\n border-radius: 6px;\r\n font-size: 13px;\r\n color: $cTextDark;\r\n cursor: pointer;\r\n user-select: none;\r\n transition: background 0.15s;\r\n\r\n &:hover {\r\n background: #F0F4FF;\r\n }\r\n\r\n input[type=\"checkbox\"] {\r\n width: 15px;\r\n height: 15px;\r\n cursor: pointer;\r\n flex-shrink: 0;\r\n }\r\n\r\n .campaigns-list-item-name {\r\n white-space: nowrap;\r\n }\r\n }\r\n }\r\n }\r\n\r\n .campaigns-chart-wrap {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n padding: 20px;\r\n margin-bottom: 20px;\r\n min-height: 350px;\r\n overflow: hidden;\r\n\r\n #container {\r\n max-width: 100%;\r\n }\r\n }\r\n\r\n .campaigns-table-wrap {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n overflow-x: auto;\r\n -ms-overflow-style: none;\r\n scrollbar-width: none;\r\n max-width: 100%;\r\n\r\n &::-webkit-scrollbar {\r\n display: none;\r\n }\r\n\r\n .table {\r\n margin: 0;\r\n width: 100% !important;\r\n\r\n thead th {\r\n background: #F8FAFC;\r\n border-bottom: 2px solid $cBorder;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.5px;\r\n color: #8899A6;\r\n padding: 12px 16px;\r\n white-space: nowrap;\r\n }\r\n\r\n tbody td {\r\n padding: 10px 16px;\r\n vertical-align: middle;\r\n border-bottom: 1px solid #F1F5F9;\r\n font-size: 13px;\r\n }\r\n\r\n tbody tr:hover {\r\n background: #F8FAFC;\r\n }\r\n }\r\n\r\n // DataTables 2.x overrides\r\n .dt-layout-row {\r\n padding: 14px 20px;\r\n margin: 0 !important;\r\n border-top: 1px solid #F1F5F9;\r\n\r\n // Ukryj wiersz z search/length jeśli pusty\r\n &:first-child {\r\n display: none;\r\n }\r\n }\r\n\r\n .dt-info {\r\n font-size: 13px;\r\n color: #8899A6;\r\n }\r\n\r\n .dt-paging {\r\n .pagination {\r\n margin: 0;\r\n padding: 0;\r\n list-style: none;\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n\r\n .page-item {\r\n .page-link {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n min-width: 36px;\r\n width: fit-content;\r\n height: 36px;\r\n padding: 0 14px;\r\n border-radius: 8px;\r\n font-size: 13px;\r\n font-weight: 500;\r\n border: 1px solid $cBorder;\r\n background: $cWhite;\r\n color: $cText;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n text-decoration: none;\r\n line-height: 1;\r\n white-space: nowrap;\r\n\r\n &:hover {\r\n background: #EEF2FF;\r\n color: $cPrimary;\r\n border-color: $cPrimary;\r\n }\r\n }\r\n\r\n &.active .page-link {\r\n background: $cPrimary;\r\n color: $cWhite;\r\n border-color: $cPrimary;\r\n font-weight: 600;\r\n }\r\n\r\n &.disabled .page-link {\r\n opacity: 0.35;\r\n cursor: default;\r\n pointer-events: none;\r\n }\r\n }\r\n }\r\n }\r\n\r\n .dt-processing {\r\n background: rgba($cWhite, 0.9);\r\n color: $cText;\r\n font-size: 14px;\r\n }\r\n }\r\n\r\n .delete-history-entry {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 30px;\r\n height: 30px;\r\n border-radius: 6px;\r\n border: none;\r\n cursor: pointer;\r\n font-size: 12px;\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: $cDanger;\r\n color: $cWhite;\r\n }\r\n }\r\n}\r\n\r\n// ===========================\r\n// PRODUCTS PAGE\r\n// ===========================\r\n.products-page {\r\n .products-header {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 20px;\r\n\r\n h2 {\r\n margin: 0;\r\n font-size: 20px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n\r\n i {\r\n color: $cPrimary;\r\n margin-right: 8px;\r\n }\r\n }\r\n }\r\n\r\n .products-filters {\r\n display: flex;\r\n flex-wrap: wrap;\r\n align-items: flex-end;\r\n gap: 20px;\r\n margin-bottom: 16px;\r\n\r\n .filter-group {\r\n flex: 1 1 220px;\r\n min-width: 0;\r\n\r\n label {\r\n display: block;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.5px;\r\n color: #8899A6;\r\n margin-bottom: 6px;\r\n\r\n i {\r\n margin-right: 4px;\r\n }\r\n }\r\n\r\n .form-control {\r\n width: 100%;\r\n padding: 10px 14px;\r\n border: 1px solid $cBorder;\r\n border-radius: 8px;\r\n font-size: 14px;\r\n color: $cTextDark;\r\n background: $cWhite;\r\n transition: border-color 0.2s;\r\n\r\n &:focus {\r\n outline: none;\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);\r\n }\r\n }\r\n\r\n select.form-control {\r\n appearance: none;\r\n -webkit-appearance: none;\r\n 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\");\r\n background-repeat: no-repeat;\r\n background-position: right 12px center;\r\n padding-right: 32px;\r\n }\r\n\r\n &.filter-group-client,\r\n &.filter-group-campaign,\r\n &.filter-group-ad-group {\r\n flex: 1 1 260px;\r\n }\r\n\r\n &.filter-group-roas {\r\n flex: 0 0 200px;\r\n }\r\n\r\n &.filter-group-columns {\r\n flex: 0 0 240px;\r\n }\r\n }\r\n }\r\n\r\n .products-actions {\r\n margin-bottom: 12px;\r\n\r\n .btn-danger {\r\n padding: 7px 14px;\r\n font-size: 13px;\r\n border-radius: 6px;\r\n border: none;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n\r\n &:disabled {\r\n opacity: 0.4;\r\n cursor: default;\r\n }\r\n }\r\n }\r\n\r\n .products-table-wrap {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n overflow: hidden;\r\n\r\n .table {\r\n margin: 0;\r\n width: 100% !important;\r\n\r\n thead th {\r\n background: #F8FAFC;\r\n border-bottom: 2px solid $cBorder;\r\n font-size: 11px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.3px;\r\n color: #8899A6;\r\n padding: 10px 8px;\r\n white-space: nowrap;\r\n }\r\n\r\n tbody td {\r\n padding: 6px 8px;\r\n vertical-align: middle;\r\n border-bottom: 1px solid #F1F5F9;\r\n font-size: 12px;\r\n }\r\n\r\n tbody tr:hover {\r\n background: #F8FAFC;\r\n }\r\n\r\n // Kompaktowe inputy w tabeli\r\n input.min_roas,\r\n input.form-control-sm,\r\n select.custom_label_4,\r\n select.form-control-sm {\r\n padding: 3px 6px;\r\n font-size: 12px;\r\n border: 1px solid $cBorder;\r\n border-radius: 4px;\r\n background: $cWhite;\r\n }\r\n }\r\n\r\n // DataTables 2.x overrides (identyczne z campaigns)\r\n .dt-layout-row {\r\n padding: 14px 20px;\r\n margin: 0 !important;\r\n border-top: 1px solid #F1F5F9;\r\n\r\n &:first-child {\r\n display: none;\r\n }\r\n }\r\n\r\n .dt-info {\r\n font-size: 13px;\r\n color: #8899A6;\r\n }\r\n\r\n .dt-paging {\r\n .pagination {\r\n margin: 0;\r\n padding: 0;\r\n list-style: none;\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n\r\n .page-item {\r\n .page-link {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n min-width: 36px;\r\n height: 36px;\r\n padding: 0 14px;\r\n border-radius: 8px;\r\n font-size: 13px;\r\n font-weight: 500;\r\n border: 1px solid $cBorder;\r\n background: $cWhite;\r\n color: $cText;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n text-decoration: none;\r\n line-height: 1;\r\n white-space: nowrap;\r\n\r\n &:hover {\r\n background: #EEF2FF;\r\n color: $cPrimary;\r\n border-color: $cPrimary;\r\n }\r\n }\r\n\r\n &.active .page-link {\r\n background: $cPrimary;\r\n color: $cWhite;\r\n border-color: $cPrimary;\r\n font-weight: 600;\r\n }\r\n\r\n &.disabled .page-link {\r\n opacity: 0.35;\r\n cursor: default;\r\n pointer-events: none;\r\n }\r\n }\r\n }\r\n }\r\n\r\n .dt-processing {\r\n background: rgba($cWhite, 0.9);\r\n color: $cText;\r\n font-size: 14px;\r\n }\r\n }\r\n\r\n // Przycisk usuwania w wierszu\r\n .delete-product {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 28px;\r\n height: 28px;\r\n border-radius: 6px;\r\n border: none;\r\n cursor: pointer;\r\n font-size: 12px;\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: $cDanger;\r\n color: $cWhite;\r\n }\r\n }\r\n\r\n // Przycisk edycji w wierszu\r\n .edit-product-title {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 28px;\r\n height: 28px;\r\n border-radius: 6px;\r\n border: none;\r\n cursor: pointer;\r\n font-size: 12px;\r\n background: #EEF2FF;\r\n color: $cPrimary;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: $cPrimary;\r\n color: $cWhite;\r\n }\r\n }\r\n}\r\n\r\n// --- Popup edycji produktu: AI suggest ---\r\n.desc-header {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 4px;\r\n\r\n label {\r\n margin: 0;\r\n }\r\n}\r\n\r\n.desc-tabs {\r\n display: flex;\r\n gap: 2px;\r\n background: #eee;\r\n border-radius: 6px;\r\n padding: 2px;\r\n}\r\n\r\n.desc-tab {\r\n border: none;\r\n background: transparent;\r\n padding: 4px 12px;\r\n font-size: 12px;\r\n border-radius: 4px;\r\n cursor: pointer;\r\n color: #666;\r\n transition: all .15s ease;\r\n\r\n i {\r\n margin-right: 4px;\r\n }\r\n\r\n &.active {\r\n background: #fff;\r\n color: #333;\r\n box-shadow: 0 1px 3px rgba(0, 0, 0, .12);\r\n font-weight: 500;\r\n }\r\n\r\n &:hover:not(.active) {\r\n color: #333;\r\n }\r\n}\r\n\r\n.desc-wrap {\r\n flex: 1;\r\n min-width: 0;\r\n}\r\n\r\n.desc-preview {\r\n\r\n ul,\r\n ol {\r\n margin: 6px 0;\r\n padding-left: 20px;\r\n }\r\n\r\n li {\r\n margin-bottom: 3px;\r\n }\r\n\r\n b,\r\n strong {\r\n font-weight: 600;\r\n }\r\n}\r\n\r\n.input-with-ai {\r\n display: flex;\r\n gap: 8px;\r\n align-items: flex-start;\r\n\r\n .form-control {\r\n flex: 1;\r\n }\r\n}\r\n\r\n.btn-ai-suggest {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 4px;\r\n padding: 6px 12px;\r\n border-radius: 8px;\r\n border: 1px solid #C084FC;\r\n background: linear-gradient(135deg, #F3E8FF, #EDE9FE);\r\n color: #7C3AED;\r\n font-size: 12px;\r\n font-weight: 600;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n white-space: nowrap;\r\n min-height: 38px;\r\n\r\n i {\r\n font-size: 13px;\r\n }\r\n\r\n &:hover {\r\n background: linear-gradient(135deg, #7C3AED, #6D28D9);\r\n color: #FFF;\r\n border-color: #6D28D9;\r\n }\r\n\r\n &:disabled {\r\n opacity: 0.7;\r\n cursor: wait;\r\n }\r\n\r\n &.btn-ai-claude {\r\n border-color: #D97706;\r\n background: linear-gradient(135deg, #FEF3C7, #FDE68A);\r\n color: #92400E;\r\n\r\n &:hover {\r\n background: linear-gradient(135deg, #D97706, #B45309);\r\n color: #FFF;\r\n border-color: #B45309;\r\n }\r\n }\r\n}\r\n\r\n// --- Form container ---\r\n.form_container {\r\n background: $cWhite;\r\n padding: 25px;\r\n max-width: 1300px;\r\n border-radius: 8px;\r\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);\r\n\r\n &.full {\r\n max-width: 100%;\r\n }\r\n\r\n .form_group {\r\n margin-bottom: 12px;\r\n display: flex;\r\n\r\n >.label {\r\n width: 300px;\r\n display: inline-flex;\r\n align-items: flex-start;\r\n justify-content: right;\r\n padding-right: 12px;\r\n }\r\n\r\n .input {\r\n width: calc(100% - 300px);\r\n }\r\n }\r\n}\r\n\r\n// --- Default popup ---\r\n.default_popup {\r\n position: fixed;\r\n top: 0;\r\n left: 0;\r\n width: 100%;\r\n height: 100%;\r\n background: rgba(0, 0, 0, 0.45);\r\n display: none;\r\n z-index: 2000;\r\n\r\n .popup_content {\r\n position: absolute;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%);\r\n background: $cWhite;\r\n padding: 25px;\r\n border-radius: 10px;\r\n max-width: 1140px;\r\n width: 95%;\r\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);\r\n\r\n .popup_header {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 15px;\r\n\r\n .title {\r\n font-size: 18px;\r\n font-weight: 600;\r\n }\r\n }\r\n\r\n .close {\r\n cursor: pointer;\r\n color: #A0AEC0;\r\n font-size: 18px;\r\n padding: 4px;\r\n\r\n &:hover {\r\n color: $cDanger;\r\n }\r\n }\r\n }\r\n}\r\n\r\n// --- DataTables override ---\r\n.dt-layout-table {\r\n margin-bottom: 20px;\r\n}\r\n\r\n.pagination {\r\n button {\r\n border: 1px solid $cBorder;\r\n background: $cWhite;\r\n display: inline-flex;\r\n height: 32px;\r\n width: 32px;\r\n align-items: center;\r\n justify-content: center;\r\n margin: 0 2px;\r\n border-radius: 4px;\r\n transition: all 0.2s;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n background: $cContentBg;\r\n border-color: $cPrimary;\r\n }\r\n }\r\n}\r\n\r\n// ===========================\r\n// PRODUCTS specific\r\n// ===========================\r\ntable#products {\r\n a {\r\n color: inherit;\r\n text-decoration: none;\r\n }\r\n\r\n .table-product-title {\r\n display: flex;\r\n justify-content: space-between;\r\n }\r\n\r\n .edit-product-title {\r\n display: flex;\r\n height: 25px;\r\n align-items: center;\r\n justify-content: center;\r\n width: 25px;\r\n cursor: pointer;\r\n background: $cWhite;\r\n border: 1px solid #CBD5E0;\r\n color: #CBD5E0;\r\n border-radius: 4px;\r\n\r\n &:hover {\r\n background: #CBD5E0;\r\n color: $cWhite;\r\n }\r\n }\r\n\r\n a.custom_name {\r\n color: $cGreenLight !important;\r\n }\r\n}\r\n\r\n// --- Product history ---\r\n.product-history-page {\r\n .product-history-meta {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 8px;\r\n margin-bottom: 14px;\r\n\r\n span {\r\n display: inline-flex;\r\n align-items: center;\r\n padding: 5px 10px;\r\n border-radius: 999px;\r\n font-size: 12px;\r\n font-weight: 600;\r\n color: $cText;\r\n background: #EEF2FF;\r\n border: 1px solid #D9E2FF;\r\n }\r\n }\r\n\r\n .product-history-chart-wrap {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n padding: 20px;\r\n margin-bottom: 16px;\r\n }\r\n\r\n .chart-with-form {\r\n display: flex;\r\n gap: 20px;\r\n align-items: flex-start;\r\n }\r\n\r\n .chart-area {\r\n flex: 1 1 auto;\r\n min-width: 0;\r\n }\r\n\r\n .product-history-chart {\r\n min-height: 360px;\r\n }\r\n\r\n .comment-form {\r\n width: 340px;\r\n flex: 0 0 340px;\r\n background: #F8FAFC;\r\n border: 1px solid $cBorder;\r\n border-radius: 10px;\r\n padding: 14px;\r\n\r\n .form-group {\r\n margin-bottom: 12px;\r\n }\r\n\r\n label {\r\n display: block;\r\n font-weight: 600;\r\n margin-bottom: 6px;\r\n font-size: 13px;\r\n color: #52606D;\r\n }\r\n\r\n input[type=\"date\"],\r\n textarea {\r\n width: 100%;\r\n border: 1px solid $cBorder;\r\n border-radius: 6px;\r\n padding: 8px 12px;\r\n font-size: 14px;\r\n font-family: \"Roboto\", sans-serif;\r\n background: $cWhite;\r\n\r\n &:focus {\r\n outline: none;\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);\r\n }\r\n }\r\n\r\n textarea {\r\n min-height: 110px;\r\n resize: vertical;\r\n }\r\n\r\n .btn {\r\n width: 100%;\r\n justify-content: center;\r\n padding: 10px 16px;\r\n }\r\n\r\n .btn[disabled] {\r\n opacity: 0.6;\r\n cursor: not-allowed;\r\n }\r\n }\r\n\r\n .products-table-wrap {\r\n overflow-x: auto;\r\n\r\n .table {\r\n min-width: 980px;\r\n }\r\n\r\n .comment-cell {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 10px;\r\n }\r\n\r\n .comment-text {\r\n word-break: break-word;\r\n }\r\n\r\n .delete-comment {\r\n color: $cDanger;\r\n text-decoration: none;\r\n font-weight: 600;\r\n white-space: nowrap;\r\n\r\n &:hover {\r\n text-decoration: underline;\r\n }\r\n }\r\n\r\n .dt-paging .pagination .page-item {\r\n list-style: none;\r\n }\r\n }\r\n}\r\n\r\n.cron-status-overview {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 10px 20px;\r\n margin-bottom: 20px;\r\n color: $cText;\r\n font-size: 13px;\r\n}\r\n\r\n.cron-progress-list {\r\n margin-bottom: 20px;\r\n}\r\n\r\n.cron-progress-item {\r\n margin-bottom: 14px;\r\n\r\n &:last-child {\r\n margin-bottom: 0;\r\n }\r\n\r\n .cron-progress-head {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n gap: 12px;\r\n margin-bottom: 6px;\r\n font-size: 13px;\r\n\r\n strong {\r\n color: $cTextDark;\r\n font-weight: 600;\r\n }\r\n\r\n span {\r\n color: #6B7A89;\r\n font-size: 12px;\r\n font-weight: 600;\r\n white-space: nowrap;\r\n }\r\n }\r\n\r\n small {\r\n display: block;\r\n margin-top: 5px;\r\n color: #778899;\r\n font-size: 12px;\r\n }\r\n}\r\n\r\n.cron-progress-bar {\r\n width: 100%;\r\n height: 10px;\r\n border-radius: 999px;\r\n background: #E9EEF5;\r\n overflow: hidden;\r\n\r\n >span {\r\n display: block;\r\n height: 100%;\r\n background: linear-gradient(90deg, #5A9BFF 0%, #2E6BDF 100%);\r\n }\r\n}\r\n\r\n.cron-url-list {\r\n margin-bottom: 20px;\r\n}\r\n\r\n.cron-url-item {\r\n border: 1px solid $cBorder;\r\n border-radius: 8px;\r\n background: #F8FAFC;\r\n padding: 10px 12px;\r\n margin-bottom: 10px;\r\n\r\n &:last-child {\r\n margin-bottom: 0;\r\n }\r\n\r\n .cron-url-top {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n gap: 8px;\r\n margin-bottom: 6px;\r\n\r\n strong {\r\n color: $cTextDark;\r\n font-size: 13px;\r\n font-weight: 600;\r\n }\r\n\r\n small {\r\n color: #7A8794;\r\n font-size: 11px;\r\n white-space: nowrap;\r\n }\r\n }\r\n\r\n code {\r\n display: block;\r\n background: #EEF2F7;\r\n border: 1px solid #DDE4ED;\r\n border-radius: 6px;\r\n padding: 6px 8px;\r\n color: #2E3B49;\r\n font-size: 12px;\r\n overflow-x: auto;\r\n }\r\n}\r\n\r\n@media (max-width: 1200px) {\r\n .product-history-page {\r\n .chart-with-form {\r\n flex-direction: column;\r\n }\r\n\r\n .comment-form {\r\n width: 100%;\r\n flex: 1 1 auto;\r\n }\r\n }\r\n}\r\n\r\n// --- Select2 w modalu ---\r\n.jconfirm-box .form-group .select2-container,\r\n.adspro-dialog-box .form-group .select2-container {\r\n width: 100% !important;\r\n margin-top: 8px;\r\n}\r\n\r\n.jconfirm-box .select2-container--default .select2-selection--single,\r\n.adspro-dialog-box .select2-container--default .select2-selection--single {\r\n background-color: $cWhite;\r\n border: 1px solid $cBorder;\r\n border-radius: 6px;\r\n min-height: 42px;\r\n display: flex;\r\n align-items: center;\r\n padding: 4px 12px;\r\n box-shadow: none;\r\n transition: border-color 0.2s, box-shadow 0.2s;\r\n font-size: 14px;\r\n}\r\n\r\n.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__rendered,\r\n.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__rendered {\r\n padding-left: 0;\r\n line-height: 1.4;\r\n color: #495057;\r\n}\r\n\r\n.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__placeholder,\r\n.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__placeholder {\r\n color: #CBD5E0;\r\n}\r\n\r\n.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__arrow,\r\n.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__arrow {\r\n height: 100%;\r\n right: 8px;\r\n}\r\n\r\n.jconfirm-box .select2-container--default.select2-container--focus .select2-selection--single,\r\n.jconfirm-box .select2-container--default .select2-selection--single:hover,\r\n.adspro-dialog-box .select2-container--default.select2-container--focus .select2-selection--single,\r\n.adspro-dialog-box .select2-container--default .select2-selection--single:hover {\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);\r\n outline: 0;\r\n}\r\n\r\n.jconfirm-box .select2-container .select2-dropdown,\r\n.adspro-dialog-box .select2-container .select2-dropdown {\r\n border-color: $cBorder;\r\n border-radius: 0 0 6px 6px;\r\n font-size: 14px;\r\n}\r\n\r\n.jconfirm-box .select2-container .select2-search--dropdown .select2-search__field,\r\n.adspro-dialog-box .select2-container .select2-search--dropdown .select2-search__field {\r\n padding: 6px 10px;\r\n border-radius: 4px;\r\n border: 1px solid $cBorder;\r\n font-size: 14px;\r\n}\r\n\r\n.jconfirm-box .select2-container--default .select2-results__option--highlighted[aria-selected],\r\n.adspro-dialog-box .select2-container--default .select2-results__option--highlighted[aria-selected] {\r\n background-color: $cPrimary;\r\n color: $cWhite;\r\n}\r\n\r\n// ===========================\r\n// RESPONSYWNOŚĆ\r\n// ===========================\r\n@media (max-width: 992px) {\r\n .sidebar {\r\n transform: translateX(-100%);\r\n\r\n &.mobile-open {\r\n transform: translateX(0);\r\n }\r\n }\r\n\r\n .main-wrapper {\r\n margin-left: 0 !important;\r\n }\r\n}\r\n\r\n// ===========================\r\n// CAMPAIGN TERMS VIEW\r\n// ===========================\r\n.campaign-terms-wrap {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 20px;\r\n margin-top: 20px;\r\n}\r\n\r\n.campaign-terms-page {\r\n max-width: 100%;\r\n overflow: hidden;\r\n\r\n .campaigns-filters {\r\n flex-wrap: wrap;\r\n\r\n .filter-group {\r\n min-width: 220px;\r\n\r\n &.terms-columns-group {\r\n min-width: 280px;\r\n }\r\n }\r\n }\r\n\r\n .terms-card-toggle {\r\n margin-left: auto;\r\n width: 28px;\r\n height: 28px;\r\n border: 1px solid #E2E8F0;\r\n border-radius: 6px;\r\n background: #FFFFFF;\r\n color: #475569;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: #F8FAFC;\r\n border-color: #CBD5E1;\r\n }\r\n }\r\n\r\n .terms-adgroups-card.is-collapsed .campaigns-extra-table-wrap {\r\n display: none;\r\n }\r\n\r\n .terms-search-toolbar {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n padding: 10px 12px;\r\n border-bottom: 1px solid #EEF2F7;\r\n background: #FFFFFF;\r\n\r\n label {\r\n font-size: 12px;\r\n font-weight: 600;\r\n color: #475569;\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin: 0;\r\n white-space: nowrap;\r\n }\r\n\r\n .terms-search-toolbar-label {\r\n min-width: 86px;\r\n }\r\n\r\n #terms_min_clicks_all,\r\n #terms_max_clicks_all {\r\n width: 160px;\r\n height: 32px;\r\n }\r\n\r\n #terms_min_conversions_all,\r\n #terms_max_conversions_all {\r\n width: 130px;\r\n max-width: 130px;\r\n }\r\n }\r\n\r\n .terms-search-selected-label {\r\n margin: 0;\r\n font-size: 12px;\r\n color: #475569;\r\n font-weight: 600;\r\n white-space: nowrap;\r\n }\r\n\r\n .terms-ai-analyze-btn {\r\n margin-left: auto;\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n height: 32px;\r\n padding: 0 12px;\r\n border-radius: 6px;\r\n border: 1px solid #BFDBFE;\r\n background: #EFF6FF;\r\n color: #1D4ED8;\r\n font-size: 12px;\r\n font-weight: 600;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: #DBEAFE;\r\n border-color: #93C5FD;\r\n }\r\n\r\n &:disabled {\r\n opacity: 0.6;\r\n cursor: wait;\r\n }\r\n }\r\n\r\n .terms-negative-toolbar {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n padding: 10px 12px;\r\n border-bottom: 1px solid #EEF2F7;\r\n background: #FFFFFF;\r\n }\r\n\r\n .terms-negative-bulk-btn {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n height: 32px;\r\n padding: 0 12px;\r\n border-radius: 6px;\r\n border: 1px solid #FECACA;\r\n background: #FEF2F2;\r\n color: #DC2626;\r\n font-size: 12px;\r\n font-weight: 600;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: #FEE2E2;\r\n border-color: #FCA5A5;\r\n }\r\n\r\n &:disabled {\r\n opacity: 0.5;\r\n cursor: not-allowed;\r\n }\r\n }\r\n\r\n table.campaigns-extra-table>thead>tr>th {\r\n position: sticky;\r\n top: 0;\r\n z-index: 2;\r\n background-color: #111827 !important;\r\n color: #E5E7EB !important;\r\n border-bottom: 1px solid #0B1220 !important;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: .4px;\r\n padding: 10px 12px;\r\n white-space: nowrap;\r\n }\r\n\r\n #terms_search_table thead th .dt-column-order,\r\n #terms_negative_table thead th .dt-column-order {\r\n display: none !important;\r\n }\r\n\r\n #terms_search_table thead th.dt-orderable-asc,\r\n #terms_search_table thead th.dt-orderable-desc,\r\n #terms_negative_table thead th.dt-orderable-asc,\r\n #terms_negative_table thead th.dt-orderable-desc {\r\n cursor: pointer;\r\n padding-right: 34px;\r\n overflow: hidden;\r\n }\r\n\r\n #terms_search_table thead th .dt-column-title,\r\n #terms_negative_table thead th .dt-column-title {\r\n display: block;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n padding-right: 2px;\r\n }\r\n\r\n #terms_search_table thead th.dt-orderable-asc::after,\r\n #terms_search_table thead th.dt-orderable-desc::after,\r\n #terms_negative_table thead th.dt-orderable-asc::after,\r\n #terms_negative_table thead th.dt-orderable-desc::after {\r\n content: '\\2195';\r\n position: absolute;\r\n right: 10px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n width: 16px;\r\n height: 16px;\r\n border-radius: 999px;\r\n font-size: 12px;\r\n font-weight: 700;\r\n line-height: 16px;\r\n text-align: center;\r\n color: #E5E7EB;\r\n background: #374151;\r\n }\r\n\r\n #terms_search_table thead th.dt-ordering-asc::after,\r\n #terms_negative_table thead th.dt-ordering-asc::after,\r\n #terms_search_table thead th[aria-sort=\"ascending\"]::after,\r\n #terms_negative_table thead th[aria-sort=\"ascending\"]::after {\r\n content: '\\25B2';\r\n color: #FFFFFF;\r\n background: #2563EB;\r\n }\r\n\r\n #terms_search_table thead th.dt-ordering-desc::after,\r\n #terms_negative_table thead th.dt-ordering-desc::after,\r\n #terms_search_table thead th[aria-sort=\"descending\"]::after,\r\n #terms_negative_table thead th[aria-sort=\"descending\"]::after {\r\n content: '\\25BC';\r\n color: #FFFFFF;\r\n background: #2563EB;\r\n }\r\n\r\n #terms_negative_select_all,\r\n .terms-negative-select-row,\r\n #terms_search_select_all,\r\n .terms-search-select-row {\r\n width: 14px;\r\n height: 14px;\r\n cursor: pointer;\r\n }\r\n\r\n .dt-layout-row:first-child {\r\n display: none;\r\n }\r\n\r\n .dt-layout-row {\r\n padding: 10px 12px;\r\n margin: 0 !important;\r\n border-top: 1px solid #F1F5F9;\r\n }\r\n\r\n .dt-info {\r\n font-size: 12px;\r\n color: #64748B;\r\n }\r\n\r\n .dt-paging .pagination {\r\n margin: 0;\r\n padding: 0;\r\n list-style: none !important;\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n\r\n .page-item {\r\n list-style: none !important;\r\n\r\n .page-link {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n min-width: 36px;\r\n width: fit-content;\r\n height: 32px;\r\n padding: 0 12px;\r\n border-radius: 6px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n border: 1px solid #E2E8F0;\r\n background: #FFFFFF;\r\n color: #4E5E6A;\r\n text-decoration: none;\r\n line-height: 1;\r\n white-space: nowrap;\r\n\r\n &:hover {\r\n background: #EEF2FF;\r\n color: #6690F4;\r\n border-color: #6690F4;\r\n }\r\n }\r\n\r\n &.previous .page-link,\r\n &.next .page-link {\r\n min-width: 72px;\r\n }\r\n\r\n &.active .page-link {\r\n background: #6690F4;\r\n color: #FFFFFF;\r\n border-color: #6690F4;\r\n }\r\n\r\n &.disabled .page-link {\r\n opacity: 0.35;\r\n cursor: default;\r\n pointer-events: none;\r\n }\r\n }\r\n }\r\n}\r\n\r\n.terms-columns-box {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 6px;\r\n}\r\n\r\n.terms-columns-control {\r\n border: 1px solid #E2E8F0;\r\n border-radius: 6px;\r\n background: #FFFFFF;\r\n overflow: hidden;\r\n\r\n summary {\r\n cursor: pointer;\r\n padding: 8px 10px;\r\n font-size: 12px;\r\n font-weight: 600;\r\n color: #334155;\r\n list-style: none;\r\n\r\n &::-webkit-details-marker {\r\n display: none;\r\n }\r\n\r\n &::after {\r\n content: '\\25BC';\r\n float: right;\r\n font-size: 10px;\r\n color: #64748B;\r\n margin-top: 2px;\r\n }\r\n }\r\n\r\n &[open] summary::after {\r\n content: '\\25B2';\r\n }\r\n}\r\n\r\n.terms-columns-list {\r\n border-top: 1px solid #EEF2F7;\r\n padding: 8px 10px;\r\n max-height: 180px;\r\n overflow-y: auto;\r\n\r\n .terms-col-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n font-size: 12px;\r\n color: #334155;\r\n margin-bottom: 6px;\r\n\r\n &:last-child {\r\n margin-bottom: 0;\r\n }\r\n\r\n input[type=checkbox] {\r\n margin: 0;\r\n }\r\n }\r\n}\r\n\r\n.campaigns-extra-card {\r\n background: #FFFFFF;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n overflow: hidden;\r\n}\r\n\r\n.campaigns-extra-card-title {\r\n padding: 14px 16px;\r\n border-bottom: 1px solid #E2E8F0;\r\n font-size: 13px;\r\n font-weight: 700;\r\n color: #334155;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n\r\n .terms-card-title-label {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 8px;\r\n }\r\n}\r\n\r\n.campaigns-extra-table-wrap {\r\n overflow: auto;\r\n}\r\n\r\n.campaigns-extra-table {\r\n margin: 0;\r\n width: 100%;\r\n table-layout: fixed;\r\n\r\n tbody td {\r\n padding: 9px 12px;\r\n border-bottom: 1px solid #F1F5F9;\r\n font-size: 13px;\r\n color: #334155;\r\n vertical-align: middle;\r\n white-space: nowrap;\r\n }\r\n\r\n td.num-cell {\r\n text-align: right;\r\n white-space: nowrap;\r\n }\r\n\r\n td.text-cell {\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n }\r\n\r\n th.terms-negative-select-cell,\r\n td.terms-negative-select-cell,\r\n th.terms-search-select-cell,\r\n td.terms-search-select-cell {\r\n text-align: center;\r\n }\r\n\r\n th.phrase-nowrap,\r\n td.phrase-nowrap {\r\n white-space: nowrap !important;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n }\r\n\r\n .terms-add-negative-btn,\r\n .terms-remove-negative-btn {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 28px;\r\n height: 28px;\r\n border-radius: 6px;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n }\r\n\r\n .terms-add-negative-btn {\r\n border: 1px solid #E2E8F0;\r\n background: #EEF2FF;\r\n color: #3B82F6;\r\n\r\n &:hover {\r\n background: #3B82F6;\r\n color: #FFFFFF;\r\n border-color: #3B82F6;\r\n }\r\n }\r\n\r\n .terms-remove-negative-btn {\r\n border: 1px solid #FECACA;\r\n background: #FEF2F2;\r\n color: #DC2626;\r\n\r\n &:hover {\r\n background: #DC2626;\r\n color: #FFFFFF;\r\n border-color: #DC2626;\r\n }\r\n }\r\n\r\n tbody tr:hover {\r\n background: #F8FAFC;\r\n }\r\n\r\n tbody tr.term-is-negative td {\r\n color: #DC2626;\r\n }\r\n\r\n tbody tr.term-is-negative:hover {\r\n background: #FEF2F2;\r\n }\r\n}\r\n\r\n.campaigns-empty-row {\r\n text-align: center;\r\n color: #94A3B8 !important;\r\n font-style: italic;\r\n}\r\n\r\n.terms-ai-modal-toolbar {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n margin-bottom: 10px;\r\n\r\n label {\r\n font-size: 12px;\r\n font-weight: 600;\r\n color: #334155;\r\n margin: 0;\r\n }\r\n\r\n .form-control {\r\n width: 200px;\r\n height: 32px;\r\n }\r\n}\r\n\r\n.terms-ai-summary {\r\n font-size: 12px;\r\n color: #64748B;\r\n margin-bottom: 10px;\r\n}\r\n\r\n.terms-ai-results-wrap {\r\n border: 1px solid #E2E8F0;\r\n border-radius: 8px;\r\n max-height: 420px;\r\n overflow: auto;\r\n}\r\n\r\n.terms-ai-results-table {\r\n width: 100%;\r\n border-collapse: collapse;\r\n font-size: 12px;\r\n\r\n th,\r\n td {\r\n border-bottom: 1px solid #EEF2F7;\r\n padding: 8px;\r\n vertical-align: middle;\r\n }\r\n\r\n th {\r\n position: sticky;\r\n top: 0;\r\n background: #F8FAFC;\r\n color: #334155;\r\n font-weight: 700;\r\n }\r\n\r\n td.term-col {\r\n min-width: 260px;\r\n max-width: 380px;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n }\r\n\r\n td.reason-col {\r\n min-width: 320px;\r\n }\r\n}\r\n\r\n.terms-ai-action-badge {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border-radius: 999px;\r\n padding: 2px 8px;\r\n font-size: 11px;\r\n font-weight: 700;\r\n\r\n &.action-exclude {\r\n background: #FEE2E2;\r\n color: #B91C1C;\r\n }\r\n\r\n &.action-keep {\r\n background: #DCFCE7;\r\n color: #166534;\r\n }\r\n}\r\n\r\n// ===========================\r\n// PRODUCTS VIEW (INLINE MOVED)\r\n// ===========================\r\n.products-page .products-filters .filter-group.filter-group-columns {\r\n min-width: 240px;\r\n}\r\n\r\n.products-columns-control {\r\n border: 1px solid #E2E8F0;\r\n border-radius: 6px;\r\n background: #FFFFFF;\r\n overflow: hidden;\r\n\r\n summary {\r\n cursor: pointer;\r\n padding: 8px 10px;\r\n font-size: 12px;\r\n font-weight: 600;\r\n color: #334155;\r\n list-style: none;\r\n\r\n &::-webkit-details-marker {\r\n display: none;\r\n }\r\n\r\n &::after {\r\n content: '\\25BC';\r\n float: right;\r\n font-size: 10px;\r\n color: #64748B;\r\n margin-top: 2px;\r\n }\r\n }\r\n\r\n &[open] summary::after {\r\n content: '\\25B2';\r\n }\r\n}\r\n\r\n.products-columns-list {\r\n border-top: 1px solid #EEF2F7;\r\n padding: 8px 10px;\r\n max-height: 220px;\r\n overflow-y: auto;\r\n\r\n .products-col-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n font-size: 12px;\r\n color: #334155;\r\n margin-bottom: 6px;\r\n\r\n &:last-child {\r\n margin-bottom: 0;\r\n }\r\n\r\n input[type=checkbox] {\r\n margin: 0;\r\n }\r\n }\r\n}\r\n\r\n#products {\r\n\r\n th:last-child,\r\n td:last-child {\r\n white-space: nowrap;\r\n }\r\n\r\n .products-row-actions {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 4px;\r\n\r\n .btn {\r\n width: 38px;\r\n height: 32px;\r\n padding: 0;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border-radius: 4px !important;\r\n\r\n i {\r\n line-height: 1;\r\n }\r\n }\r\n }\r\n}\r\n\r\n.products-page table#products>thead>tr>th {\r\n position: sticky;\r\n top: 0;\r\n z-index: 2;\r\n background-color: #111827 !important;\r\n color: #E5E7EB !important;\r\n border-bottom: 1px solid #0B1220 !important;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: .4px;\r\n padding: 10px 12px;\r\n white-space: nowrap;\r\n}\r\n\r\n.products-page #products thead th .dt-column-order {\r\n display: none !important;\r\n}\r\n\r\n.products-page #products thead th.dt-orderable-asc,\r\n.products-page #products thead th.dt-orderable-desc {\r\n cursor: pointer;\r\n padding-right: 34px;\r\n overflow: hidden;\r\n}\r\n\r\n.products-page #products thead th .dt-column-title {\r\n display: block;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n padding-right: 2px;\r\n}\r\n\r\n.products-page #products thead th.dt-orderable-asc::after,\r\n.products-page #products thead th.dt-orderable-desc::after {\r\n content: '\\2195';\r\n position: absolute;\r\n right: 10px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n width: 16px;\r\n height: 16px;\r\n border-radius: 999px;\r\n font-size: 12px;\r\n font-weight: 700;\r\n line-height: 16px;\r\n text-align: center;\r\n color: #E5E7EB;\r\n background: #374151;\r\n}\r\n\r\n.products-page #products thead th.dt-ordering-asc::after,\r\n.products-page #products thead th[aria-sort=\"ascending\"]::after {\r\n content: '\\25B2';\r\n color: #FFFFFF;\r\n background: #2563EB;\r\n}\r\n\r\n.products-page #products thead th.dt-ordering-desc::after,\r\n.products-page #products thead th[aria-sort=\"descending\"]::after {\r\n content: '\\25BC';\r\n color: #FFFFFF;\r\n background: #2563EB;\r\n}"]} \ No newline at end of file +{"version":3,"sources":["style.css","style.scss"],"names":[],"mappings":"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","file":"style.css","sourcesContent":["*{box-sizing:border-box}body{font-family:\"Roboto\",sans-serif;margin:0;padding:0;font-size:14px;color:#4e5e6a;background:#f4f6f9;max-width:100vw;overflow-x:hidden}.hide{display:none}small{font-size:.75em}.text-right{text-align:right}.text-bold{font-weight:700 !important}.nowrap{white-space:nowrap}body.unlogged{background:#f4f6f9;margin:0;padding:0}.login-container{display:flex;min-height:100vh}.login-brand{flex:0 0 45%;background:linear-gradient(135deg, #1E2A3A 0%, #2C3E57 50%, #6690F4 100%);display:flex;align-items:center;justify-content:center;padding:60px;position:relative;overflow:hidden}.login-brand::before{content:\"\";position:absolute;top:-50%;right:-50%;width:100%;height:100%;background:radial-gradient(circle, rgba(102, 144, 244, 0.15) 0%, transparent 70%);border-radius:50%}.login-brand .brand-content{position:relative;z-index:1;color:#fff;max-width:400px}.login-brand .brand-logo{font-size:48px;font-weight:300;margin-bottom:20px;letter-spacing:-1px}.login-brand .brand-logo strong{font-weight:700}.login-brand .brand-tagline{font-size:18px;opacity:.85;line-height:1.6;margin-bottom:50px}.login-brand .brand-features .feature{display:flex;align-items:center;gap:15px;margin-bottom:20px;opacity:.8}.login-brand .brand-features .feature i{font-size:20px;width:40px;height:40px;display:flex;align-items:center;justify-content:center;background:hsla(0,0%,100%,.1);border-radius:10px}.login-brand .brand-features .feature span{font-size:15px}.login-form-wrapper{flex:1;display:flex;align-items:center;justify-content:center;padding:60px;background:#fff}.login-box{width:100%;max-width:420px}.login-box .login-header{margin-bottom:35px}.login-box .login-header h1{font-size:28px;font-weight:700;color:#2d3748;margin:0 0 8px}.login-box .login-header p{color:#718096;font-size:15px;margin:0}.login-box .form-group{margin-bottom:20px}.login-box .form-group label{display:block;font-size:13px;font-weight:600;color:#2d3748;margin-bottom:6px}.login-box .input-with-icon{position:relative}.login-box .input-with-icon i{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#a0aec0;font-size:14px}.login-box .input-with-icon .form-control{padding-left:42px}.login-box .form-control{width:100%;height:46px;border:2px solid #e2e8f0;border-radius:8px;padding:0 14px;font-size:14px;font-family:\"Roboto\",sans-serif;color:#2d3748;transition:border-color .3s,box-shadow .3s}.login-box .form-control::placeholder{color:#cbd5e0}.login-box .form-control:focus{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.15);outline:none}.login-box .form-error{color:#c00;font-size:12px;margin-top:4px}.login-box .checkbox-group .checkbox-label{display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;color:#718096;font-weight:400}.login-box .checkbox-group .checkbox-label input[type=checkbox]{width:16px;height:16px;accent-color:#6690f4}.login-box .btn-login{width:100%;height:48px;font-size:15px;font-weight:600;border-radius:8px;display:flex;align-items:center;justify-content:center;gap:8px}.login-box .btn-login.disabled{opacity:.7;pointer-events:none}.login-box .alert{display:none;padding:12px 16px;border-radius:8px;font-size:13px;margin-bottom:20px}.login-box .alert.alert-danger{background:#fff5f5;color:#c00;border:1px solid #fed7d7}.login-box .alert.alert-success{background:#f0fff4;color:#276749;border:1px solid #c6f6d5}@media(max-width: 768px){.login-brand{display:none}.login-form-wrapper{padding:30px 20px}}body.logged{display:flex;min-height:100vh;background:#f4f6f9}.sidebar{width:260px;min-height:100vh;background:#1e2a3a;position:fixed;top:0;left:0;z-index:1000;display:flex;flex-direction:column;transition:width .3s ease;overflow:hidden}.sidebar.collapsed{width:70px}.sidebar.collapsed .sidebar-header{padding:16px 0;justify-content:center}.sidebar.collapsed .sidebar-header .sidebar-logo{display:none}.sidebar.collapsed .sidebar-header .sidebar-toggle i{transform:rotate(180deg)}.sidebar.collapsed .sidebar-nav ul li a{padding:12px 0;justify-content:center}.sidebar.collapsed .sidebar-nav ul li a span{display:none}.sidebar.collapsed .sidebar-nav ul li a i{margin-right:0;font-size:18px}.sidebar.collapsed .sidebar-nav ul li.nav-group .nav-group-label{padding:12px 0;justify-content:center}.sidebar.collapsed .sidebar-nav ul li.nav-group .nav-group-label span{display:none}.sidebar.collapsed .sidebar-nav ul li.nav-group .nav-group-label i{margin-right:0;font-size:18px}.sidebar.collapsed .sidebar-footer .sidebar-user{justify-content:center}.sidebar.collapsed .sidebar-footer .sidebar-user .user-info{display:none}.sidebar.collapsed .sidebar-footer .sidebar-logout{justify-content:center}.sidebar.collapsed .sidebar-footer .sidebar-logout span{display:none}.sidebar.collapsed .nav-divider{margin:8px 15px}.sidebar-header{display:flex;align-items:center;justify-content:space-between;padding:20px 20px 16px;border-bottom:1px solid hsla(0,0%,100%,.08)}.sidebar-header .sidebar-logo a{color:#fff;text-decoration:none;font-size:24px;font-weight:300;letter-spacing:-0.5px}.sidebar-header .sidebar-logo a strong{font-weight:700}.sidebar-header .sidebar-toggle{background:none;border:none;color:#a8b7c7;cursor:pointer;padding:6px;border-radius:6px;transition:all .3s}.sidebar-header .sidebar-toggle:hover{background:hsla(0,0%,100%,.08);color:#fff}.sidebar-header .sidebar-toggle i{transition:transform .3s}.sidebar-nav{flex:1;padding:12px 0;overflow-y:auto}.sidebar-nav ul{list-style:none;margin:0;padding:0}.sidebar-nav ul li.nav-group{margin-bottom:4px}.sidebar-nav ul li.nav-group .nav-group-label{display:flex;align-items:center;padding:11px 20px;color:#d5deea;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;border-left:3px solid rgba(0,0,0,0)}.sidebar-nav ul li.nav-group .nav-group-label i{width:20px;text-align:center;margin-right:12px;font-size:14px;color:#b6c4d3}.sidebar-nav ul li.nav-group .nav-submenu{margin:0;padding:0;list-style:none}.sidebar-nav ul li.nav-group .nav-submenu li a{padding-left:44px}.sidebar-nav ul li.nav-group.active>.nav-group-label{color:#fff;background:rgba(102,144,244,.12);border-left-color:#6690f4}.sidebar-nav ul li.nav-group.active>.nav-group-label i{color:#6690f4}.sidebar-nav ul li.nav-divider{height:1px;background:hsla(0,0%,100%,.08);margin:8px 20px}.sidebar-nav ul li a{display:flex;align-items:center;padding:11px 20px;color:#a8b7c7;text-decoration:none;font-size:14px;transition:all .2s;border-left:3px solid rgba(0,0,0,0)}.sidebar-nav ul li a i{width:20px;text-align:center;margin-right:12px;font-size:15px}.sidebar-nav ul li a:hover{background:#263548;color:#fff}.sidebar-nav ul li.active>a{background:rgba(102,144,244,.15);color:#fff;border-left-color:#6690f4}.sidebar-nav ul li.active>a i{color:#6690f4}.badge-alerts-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;margin-left:8px;border-radius:50%;font-size:11px;font-weight:600;line-height:1;background:#fff;color:#6690f4}.sidebar-footer{padding:16px 20px;border-top:1px solid hsla(0,0%,100%,.08)}.sidebar-footer .sidebar-user{display:flex;align-items:center;gap:10px;margin-bottom:12px}.sidebar-footer .sidebar-user .user-avatar{width:34px;height:34px;border-radius:50%;background:rgba(102,144,244,.2);display:flex;align-items:center;justify-content:center;color:#6690f4;font-size:14px;flex-shrink:0}.sidebar-footer .sidebar-user .user-info{overflow:hidden}.sidebar-footer .sidebar-user .user-info .user-email{color:#a8b7c7;font-size:12px;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.sidebar-footer .sidebar-logout{display:flex;align-items:center;gap:8px;color:#e53e3e;text-decoration:none;font-size:13px;padding:8px 10px;border-radius:6px;transition:all .2s}.sidebar-footer .sidebar-logout i{font-size:14px}.sidebar-footer .sidebar-logout:hover{background:rgba(229,62,62,.1)}.main-wrapper{margin-left:260px;flex:1;min-height:100vh;transition:margin-left .3s ease;display:flex;flex-direction:column}.main-wrapper.expanded{margin-left:70px}.topbar{height:56px;background:#fff;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;padding:0 25px;position:sticky;top:0;z-index:500}.topbar .topbar-toggle{background:none;border:none;color:#4e5e6a;cursor:pointer;padding:8px 10px;border-radius:6px;font-size:16px;margin-right:15px;transition:all .2s}.topbar .topbar-toggle:hover{background:#f4f6f9}.topbar .topbar-breadcrumb{font-size:16px;font-weight:600;color:#2d3748}.content{flex:1;padding:25px}.app-alert{background:#ebf8ff;border:1px solid #bee3f8;color:#2b6cb0;padding:12px 16px;border-radius:8px;margin-bottom:20px;font-size:14px}.btn{padding:10px 20px;transition:all .2s ease;color:#fff;border:0;border-radius:6px;cursor:pointer;display:inline-flex;text-decoration:none;gap:6px;justify-content:center;align-items:center;font-size:14px;font-family:\"Roboto\",sans-serif;font-weight:500}.btn.btn_small,.btn.btn-xs,.btn.btn-sm{padding:5px 10px;font-size:12px}.btn.btn_small i,.btn.btn-xs i,.btn.btn-sm i{font-size:11px}.btn.btn-success{background:#57b951}.btn.btn-success:hover{background:#4a9c3b}.btn.btn-primary{background:#6690f4}.btn.btn-primary:hover{background:#3164db}.btn.btn-danger{background:#c00}.btn.btn-danger:hover{background:#b30000}.btn.disabled{opacity:.6;pointer-events:none}.form-control{border:1px solid #e2e8f0;border-radius:6px;height:38px;width:100%;padding:6px 12px;font-family:\"Roboto\",sans-serif;font-size:14px;color:#2d3748;transition:border-color .2s,box-shadow .2s}.form-control option{padding:5px}.form-control:focus{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1);outline:none}input[type=checkbox]{border:1px solid #e2e8f0}table{border-collapse:collapse;font-size:13px}.table{width:100%}.table th,.table td{border:1px solid #e2e8f0;padding:8px 10px}.table th{background:#f7fafc;font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.03em;color:#718096}.table td.center{text-align:center}.table td.left{text-align:left}.table.table-sm td{padding:5px !important}.table input.form-control{font-size:13px;height:32px}.card{background:#fff;padding:20px;border-radius:8px;color:#2d3748;font-size:14px;box-shadow:0 1px 3px rgba(0,0,0,.06)}.card.mb25{margin-bottom:20px}.card .card-header{font-weight:600;font-size:15px}.card .card-body{padding-top:12px}.card .card-body table th,.card .card-body table td{font-size:13px}.card .card-body table th.bold,.card .card-body table td.bold{font-weight:600}.card .card-body table th.text-right,.card .card-body table td.text-right{text-align:right}.card .card-body table th.text-center,.card .card-body table td.text-center{text-align:center}.action_menu{display:flex;margin-bottom:20px;gap:12px}.action_menu .btn{padding:8px 16px}.action_menu .btn.btn_add{background:#57b951}.action_menu .btn.btn_add:hover{background:#4a9c3b}.action_menu .btn.btn_cancel{background:#c00}.action_menu .btn.btn_cancel:hover{background:#b30000}.settings-tabs{display:flex;gap:8px;margin-bottom:18px}.settings-tabs .settings-tab{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:8px;text-decoration:none;color:#6b7a89;background:#e9eef5;border:1px solid #d8e0ea;font-size:13px;font-weight:600;transition:all .2s}.settings-tabs .settings-tab:hover{color:#2d3748;background:#dde6f2}.settings-tabs .settings-tab.active{color:#fff;background:#6690f4;border-color:#6690f4}.settings-card{background:#fff;border-radius:10px;padding:28px;box-shadow:0 1px 4px rgba(0,0,0,.06)}.settings-card .settings-card-header{display:flex;align-items:center;gap:14px;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid #e2e8f0}.settings-card .settings-card-header .settings-card-icon{width:44px;height:44px;border-radius:10px;background:rgb(225.706097561,233.7475609756,252.893902439);color:#6690f4;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}.settings-card .settings-card-header h3{margin:0;font-size:17px;font-weight:600;color:#2d3748}.settings-card .settings-card-header small{color:#8899a6;font-size:13px}.settings-card .settings-field{margin-bottom:18px}.settings-card .settings-field label{display:block;font-size:13px;font-weight:600;color:#2d3748;margin-bottom:6px}.settings-card .settings-input-wrap{position:relative}.settings-card .settings-input-wrap .settings-input-icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#a0aec0;font-size:14px;pointer-events:none}.settings-card .settings-input-wrap .form-control{padding-left:38px}.settings-card .settings-input-wrap .settings-toggle-pw{position:absolute;right:4px;top:50%;transform:translateY(-50%);background:none;border:none;color:#a0aec0;cursor:pointer;padding:6px 10px;font-size:14px;transition:color .2s}.settings-card .settings-input-wrap .settings-toggle-pw:hover{color:#6690f4}.settings-card .settings-field .settings-toggle-label{display:inline-flex;align-items:center;gap:10px;cursor:pointer;font-size:14px;font-weight:500;user-select:none;margin-bottom:0;width:100%}.settings-card .settings-field .settings-toggle-label .settings-toggle-text{flex:1 1 auto;min-width:0;line-height:1.35}.settings-card .settings-toggle-checkbox{display:none}.settings-card .settings-toggle-checkbox+.settings-toggle-switch{display:inline-block;position:relative;width:44px;height:24px;background:#ccc;border-radius:12px;transition:background .2s;flex-shrink:0}.settings-card .settings-toggle-checkbox+.settings-toggle-switch::after{content:\"\";position:absolute;top:3px;left:3px;width:18px;height:18px;background:#fff;border-radius:50%;transition:transform .2s}.settings-card .settings-toggle-checkbox:checked+.settings-toggle-switch{background:#22c55e}.settings-card .settings-toggle-checkbox:checked+.settings-toggle-switch::after{transform:translateX(20px)}.settings-card .settings-fields-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 24px}@media(max-width: 768px){.settings-card .settings-fields-grid{grid-template-columns:1fr}}.settings-card .settings-alert-error{display:flex;align-items:center;gap:10px;background:#fff5f5;color:#c00;border:1px solid #fed7d7;border-radius:8px;padding:12px 16px;margin-bottom:20px;font-size:13px}.settings-card .settings-alert-error i{font-size:16px;flex-shrink:0}.clients-page .clients-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.clients-page .clients-header h2{margin:0;font-size:20px;font-weight:600;color:#2d3748}.clients-page .clients-header h2 i{color:#6690f4;margin-right:8px}.clients-page .clients-table-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow:hidden}.clients-page .clients-table-wrap .table{margin:0}.clients-page .clients-table-wrap .table thead th{background:#f8fafc;border-bottom:2px solid #e2e8f0;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;padding:14px 20px}.clients-page .clients-table-wrap .table tbody td{padding:14px 20px;vertical-align:middle;border-bottom:1px solid #f1f5f9}.clients-page .clients-table-wrap .table tbody tr:hover{background:#f8fafc}.clients-page .clients-table-wrap .table .client-id{color:#8899a6;font-size:13px;font-weight:600}.clients-page .clients-table-wrap .table .client-name{font-weight:600;color:#2d3748}.clients-page .badge-id{display:inline-block;background:#eef2ff;color:#6690f4;font-size:13px;font-weight:600;padding:4px 10px;border-radius:6px;font-family:monospace}.clients-page .actions-cell{text-align:center;white-space:nowrap}.clients-page .btn-icon{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:8px;border:none;cursor:pointer;font-size:14px;transition:all .2s;margin:0 2px}.clients-page .btn-icon.btn-icon-edit{background:#eef2ff;color:#6690f4}.clients-page .btn-icon.btn-icon-edit:hover{background:#6690f4;color:#fff}.clients-page .btn-icon.btn-icon-delete{background:#fff5f5;color:#c00}.clients-page .btn-icon.btn-icon-delete:hover{background:#c00;color:#fff}.clients-page .btn-icon.btn-icon-sync{background:#f0fdf4;color:#16a34a}.clients-page .btn-icon.btn-icon-sync:hover{background:#16a34a;color:#fff}.clients-page .btn-icon.btn-icon-sync:disabled{opacity:.7;cursor:wait}.clients-page .btn-icon.btn-icon-sync.is-queued{background:#fef3c7;color:#d97706}.clients-page .client-sync-bars{display:flex;flex-direction:column;gap:4px}.clients-page .client-sync-row{display:flex;align-items:center;gap:4px}.clients-page .client-sync-label{font-size:11px;font-weight:600;color:#8899a6;width:18px;flex-shrink:0}.clients-page .client-sync-track{flex:1;height:6px;border-radius:999px;background:#e9eef5;overflow:hidden}.clients-page .client-sync-fill{height:100%;border-radius:999px;background:#cbd5e0;transition:width .4s ease}.clients-page .client-sync-fill.is-active{background:linear-gradient(90deg, #5A9BFF 0%, #2E6BDF 100%)}.clients-page .client-sync-fill.is-done{background:#57b951}.clients-page .client-sync-pct{font-size:11px;font-weight:600;color:#8899a6;width:32px;text-align:right;flex-shrink:0}.clients-page .empty-state{text-align:center;padding:50px 20px !important;color:#a0aec0}.clients-page .empty-state i{font-size:40px;margin-bottom:12px;display:block}.clients-page .empty-state p{margin:0;font-size:15px}.btn-secondary{background:#e2e8f0;color:#2d3748;border:none;padding:8px 18px;border-radius:6px;font-size:14px;cursor:pointer;transition:background .2s}.btn-secondary:hover{background:#cbd5e0}.campaigns-page{max-width:100%;overflow-x:hidden;width:100%}.campaigns-page .campaigns-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.campaigns-page .campaigns-header h2{margin:0;font-size:20px;font-weight:600;color:#2d3748}.campaigns-page .campaigns-header h2 i{color:#6690f4;margin-right:8px}.campaigns-page .campaigns-filters{display:flex;flex-wrap:wrap;gap:20px;margin-bottom:20px}.campaigns-page .campaigns-filters .filter-group{flex:1;min-width:0}.campaigns-page .campaigns-filters .filter-group label{display:block;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;margin-bottom:6px}.campaigns-page .campaigns-filters .filter-group label i{margin-right:4px}.campaigns-page .campaigns-filters .filter-group .form-control{width:100%;padding:10px 14px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;color:#2d3748;background:#fff;transition:border-color .2s;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}.campaigns-page .campaigns-filters .filter-group .form-control:focus{outline:none;border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.campaigns-page .campaigns-filters .filter-group .filter-with-action{display:flex;align-items:center;gap:8px}.campaigns-page .campaigns-filters .filter-group .filter-with-action .form-control{flex:1}.campaigns-page .campaigns-filters .filter-group .filter-with-action .btn-icon{flex-shrink:0;width:42px;height:42px;display:inline-flex;align-items:center;justify-content:center;border-radius:8px;border:none;cursor:pointer;font-size:14px;transition:all .2s}.campaigns-page .campaigns-filters .filter-group .filter-with-action .btn-icon.btn-icon-delete{background:#fff5f5;color:#c00}.campaigns-page .campaigns-filters .filter-group .filter-with-action .btn-icon.btn-icon-delete:hover{background:#c00;color:#fff}.campaigns-page .campaigns-filters .filter-group-campaign-multi{flex:2 !important}.campaigns-page .campaigns-filters .campaign-dropdown{flex:1;min-width:0;position:relative}.campaigns-page .campaigns-filters .campaign-dropdown-trigger{width:100%;padding:10px 14px;padding-right:32px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;color:#2d3748;background:#fff;cursor:pointer;display:flex;align-items:center;transition:border-color .2s;position:relative;min-height:42px;box-sizing:border-box}.campaigns-page .campaigns-filters .campaign-dropdown-trigger .campaign-dropdown-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.campaigns-page .campaigns-filters .campaign-dropdown-trigger .campaign-dropdown-text.is-placeholder{color:#8899a6}.campaigns-page .campaigns-filters .campaign-dropdown-trigger .campaign-dropdown-arrow{position:absolute;right:12px;font-size:10px;color:#8899a6;transition:transform .2s}.campaigns-page .campaigns-filters .campaign-dropdown.is-open .campaign-dropdown-trigger{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.campaigns-page .campaigns-filters .campaign-dropdown.is-open .campaign-dropdown-arrow{transform:rotate(180deg)}.campaigns-page .campaigns-filters .campaign-dropdown.is-open .campaign-dropdown-menu{display:block}.campaigns-page .campaigns-filters .campaign-dropdown-menu{display:none;position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:100;max-height:280px;overflow-y:auto;background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,.1);padding:4px 0}.campaigns-page .campaigns-filters .campaign-dropdown-item{display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;font-size:14px;color:#2d3748;margin:0;transition:background .15s}.campaigns-page .campaigns-filters .campaign-dropdown-item:hover{background:#f8fafc}.campaigns-page .campaigns-filters .campaign-dropdown-item.is-checked{background:#eef2ff}.campaigns-page .campaigns-filters .campaign-dropdown-item input[type=checkbox]{width:16px;height:16px;cursor:pointer;flex-shrink:0;accent-color:#6690f4}.campaigns-page .campaigns-filters .campaign-dropdown-item span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.campaigns-page .campaigns-list-panel{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);margin-bottom:20px;overflow:hidden}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #e2e8f0;gap:12px}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left{display:flex;align-items:center;gap:8px;font-size:13px;color:#4e5e6a}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left input[type=checkbox]{width:16px;height:16px;cursor:pointer}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left label{cursor:pointer;user-select:none;margin:0}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-left .campaigns-selected-count{margin-left:12px;color:#8899a6}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-right .campaigns-bulk-delete-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;background:#fff5f5;color:#c00;transition:all .2s}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-right .campaigns-bulk-delete-btn:hover:not(:disabled){background:#c00;color:#fff}.campaigns-page .campaigns-list-panel .campaigns-list-toolbar-right .campaigns-bulk-delete-btn:disabled{opacity:.4;cursor:not-allowed}.campaigns-page .campaigns-list-panel .campaigns-list-items{display:flex;flex-wrap:wrap;gap:0;padding:8px 8px;max-height:220px;overflow-y:auto}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item{display:flex;align-items:center;gap:8px;padding:6px 12px;margin:2px;border-radius:6px;font-size:13px;color:#2d3748;cursor:pointer;user-select:none;transition:background .15s}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item:hover{background:#f0f4ff}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item input[type=checkbox]{width:15px;height:15px;cursor:pointer;flex-shrink:0}.campaigns-page .campaigns-list-panel .campaigns-list-items .campaigns-list-item .campaigns-list-item-name{white-space:nowrap}.campaigns-page .campaigns-chart-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);padding:20px;margin-bottom:20px;min-height:350px;overflow:hidden}.campaigns-page .campaigns-chart-wrap #container{max-width:100%}.campaigns-page .campaigns-table-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow-x:auto;-ms-overflow-style:none;scrollbar-width:none;max-width:100%}.campaigns-page .campaigns-table-wrap::-webkit-scrollbar{display:none}.campaigns-page .campaigns-table-wrap .table{margin:0;width:100% !important}.campaigns-page .campaigns-table-wrap .table thead th{background:#f8fafc;border-bottom:2px solid #e2e8f0;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;padding:12px 16px;white-space:nowrap}.campaigns-page .campaigns-table-wrap .table tbody td{padding:10px 16px;vertical-align:middle;border-bottom:1px solid #f1f5f9;font-size:13px}.campaigns-page .campaigns-table-wrap .table tbody tr:hover{background:#f8fafc}.campaigns-page .campaigns-table-wrap .dt-layout-row{padding:14px 20px;margin:0 !important;border-top:1px solid #f1f5f9}.campaigns-page .campaigns-table-wrap .dt-layout-row:first-child{display:none}.campaigns-page .campaigns-table-wrap .dt-info{font-size:13px;color:#8899a6}.campaigns-page .campaigns-table-wrap .dt-paging .pagination{margin:0;padding:0;list-style:none;display:flex;align-items:center;gap:6px}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .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 #e2e8f0;background:#fff;color:#4e5e6a;cursor:pointer;transition:all .2s;text-decoration:none;line-height:1;white-space:nowrap}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item .page-link:hover{background:#eef2ff;color:#6690f4;border-color:#6690f4}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item.active .page-link{background:#6690f4;color:#fff;border-color:#6690f4;font-weight:600}.campaigns-page .campaigns-table-wrap .dt-paging .pagination .page-item.disabled .page-link{opacity:.35;cursor:default;pointer-events:none}.campaigns-page .campaigns-table-wrap .dt-processing{background:hsla(0,0%,100%,.9);color:#4e5e6a;font-size:14px}.campaigns-page .delete-history-entry{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:6px;border:none;cursor:pointer;font-size:12px;background:#fff5f5;color:#c00;transition:all .2s}.campaigns-page .delete-history-entry:hover{background:#c00;color:#fff}.products-page .products-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.products-page .products-header h2{margin:0;font-size:20px;font-weight:600;color:#2d3748}.products-page .products-header h2 i{color:#6690f4;margin-right:8px}.products-page .products-filters{display:flex;flex-wrap:wrap;align-items:flex-end;gap:20px;margin-bottom:16px}.products-page .products-filters .filter-group{flex:1 1 220px;min-width:0}.products-page .products-filters .filter-group label{display:block;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#8899a6;margin-bottom:6px}.products-page .products-filters .filter-group label i{margin-right:4px}.products-page .products-filters .filter-group .form-control{width:100%;padding:10px 14px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;color:#2d3748;background:#fff;transition:border-color .2s}.products-page .products-filters .filter-group .form-control:focus{outline:none;border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.products-page .products-filters .filter-group 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}.products-page .products-filters .filter-group.filter-group-client,.products-page .products-filters .filter-group.filter-group-campaign,.products-page .products-filters .filter-group.filter-group-ad-group{flex:1 1 260px}.products-page .products-filters .filter-group.filter-group-ad-group .ad-group-filter-actions{display:flex;gap:8px;align-items:center}.products-page .products-filters .filter-group.filter-group-ad-group .ad-group-filter-actions .form-control{flex:1 1 auto;min-width:0}.products-page .products-filters .filter-group.filter-group-ad-group #delete-products-ad-group{min-width:38px;height:38px;border-radius:6px;margin:0;background:#dc3545;border:1px solid #dc3545;color:#fff;cursor:pointer}.products-page .products-filters .filter-group.filter-group-ad-group #delete-products-ad-group:hover:not(:disabled){background:#bb2d3b;border-color:#bb2d3b;color:#fff}.products-page .products-filters .filter-group.filter-group-ad-group #delete-products-ad-group:disabled{opacity:.45;cursor:default;background:#dc3545;border-color:#dc3545;color:#fff}.products-page .products-filters .filter-group.filter-group-roas{flex:0 0 200px}.products-page .products-filters .filter-group.filter-group-columns{flex:0 0 240px}.products-page .products-scope-alerts{margin-bottom:12px;border:1px solid #fecaca;background:#fef2f2;border-radius:8px;overflow:hidden}.products-page .products-scope-alerts summary{cursor:pointer;list-style:none;padding:10px 12px;font-size:13px;font-weight:600;color:#991b1b;display:flex;align-items:center;gap:8px}.products-page .products-scope-alerts summary::-webkit-details-marker{display:none}.products-page .products-scope-alerts .products-scope-alerts-list{border-top:1px solid #fecaca;background:#fff;max-height:260px;overflow:auto}.products-page .products-scope-alerts .products-scope-alert-item{padding:10px 12px;border-bottom:1px solid #f1f5f9}.products-page .products-scope-alerts .products-scope-alert-item:last-child{border-bottom:none}.products-page .products-scope-alerts .products-scope-alert-meta{display:flex;align-items:center;gap:8px;margin-bottom:4px;font-size:11px;color:#64748b}.products-page .products-scope-alerts .products-scope-alert-type{display:inline-flex;align-items:center;padding:2px 6px;border-radius:999px;background:#eef2ff;color:#4338ca;font-weight:600;text-transform:uppercase;letter-spacing:.3px}.products-page .products-scope-alerts .products-scope-alert-message{font-size:13px;color:#2d3748;line-height:1.45}.products-page .products-actions{margin-bottom:12px}.products-page .products-actions .btn-danger{padding:7px 14px;font-size:13px;border-radius:6px;border:none;cursor:pointer;transition:all .2s}.products-page .products-actions .btn-danger:disabled{opacity:.4;cursor:default}.products-page .products-table-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow:hidden}.products-page .products-table-wrap .table{margin:0;width:100% !important}.products-page .products-table-wrap .table thead th{background:#f8fafc;border-bottom:2px solid #e2e8f0;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;color:#8899a6;padding:10px 8px;white-space:nowrap}.products-page .products-table-wrap .table tbody td{padding:6px 8px;vertical-align:middle;border-bottom:1px solid #f1f5f9;font-size:12px}.products-page .products-table-wrap .table tbody tr:hover{background:#f8fafc}.products-page .products-table-wrap .table input.min_roas,.products-page .products-table-wrap .table input.form-control-sm,.products-page .products-table-wrap .table select.custom_label_4,.products-page .products-table-wrap .table select.form-control-sm{padding:3px 6px;font-size:12px;border:1px solid #e2e8f0;border-radius:4px;background:#fff}.products-page .products-table-wrap .dt-layout-row{padding:14px 20px;margin:0 !important;border-top:1px solid #f1f5f9}.products-page .products-table-wrap .dt-layout-row:first-child{display:none}.products-page .products-table-wrap .dt-info{font-size:13px;color:#8899a6}.products-page .products-table-wrap .dt-paging .pagination{margin:0;padding:0;list-style:none;display:flex;align-items:center;gap:6px}.products-page .products-table-wrap .dt-paging .pagination .page-item .page-link{display:inline-flex;align-items:center;justify-content:center;min-width:36px;height:36px;padding:0 14px;border-radius:8px;font-size:13px;font-weight:500;border:1px solid #e2e8f0;background:#fff;color:#4e5e6a;cursor:pointer;transition:all .2s;text-decoration:none;line-height:1;white-space:nowrap}.products-page .products-table-wrap .dt-paging .pagination .page-item .page-link:hover{background:#eef2ff;color:#6690f4;border-color:#6690f4}.products-page .products-table-wrap .dt-paging .pagination .page-item.active .page-link{background:#6690f4;color:#fff;border-color:#6690f4;font-weight:600}.products-page .products-table-wrap .dt-paging .pagination .page-item.disabled .page-link{opacity:.35;cursor:default;pointer-events:none}.products-page .products-table-wrap .dt-processing{background:hsla(0,0%,100%,.9);color:#4e5e6a;font-size:14px}.products-page .delete-product{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;border:none;cursor:pointer;font-size:12px;background:#fff5f5;color:#c00;transition:all .2s}.products-page .delete-product:hover{background:#c00;color:#fff}.products-page .edit-product-title{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;border:none;cursor:pointer;font-size:12px;background:#eef2ff;color:#6690f4;transition:all .2s}.products-page .edit-product-title:hover{background:#6690f4;color:#fff}.desc-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}.desc-header label{margin:0}.desc-tabs{display:flex;gap:2px;background:#eee;border-radius:6px;padding:2px}.desc-tab{border:none;background:rgba(0,0,0,0);padding:4px 12px;font-size:12px;border-radius:4px;cursor:pointer;color:#666;transition:all .15s ease}.desc-tab i{margin-right:4px}.desc-tab.active{background:#fff;color:#333;box-shadow:0 1px 3px rgba(0,0,0,.12);font-weight:500}.desc-tab:hover:not(.active){color:#333}.desc-wrap{flex:1;min-width:0}.desc-preview ul,.desc-preview ol{margin:6px 0;padding-left:20px}.desc-preview li{margin-bottom:3px}.desc-preview b,.desc-preview strong{font-weight:600}.input-with-ai{display:flex;gap:8px;align-items:flex-start}.input-with-ai .form-control{flex:1}.btn-ai-suggest{display:inline-flex;align-items:center;gap:4px;padding:6px 12px;border-radius:8px;border:1px solid #c084fc;background:linear-gradient(135deg, #F3E8FF, #EDE9FE);color:#7c3aed;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s;white-space:nowrap;min-height:38px}.btn-ai-suggest i{font-size:13px}.btn-ai-suggest:hover{background:linear-gradient(135deg, #7C3AED, #6D28D9);color:#fff;border-color:#6d28d9}.btn-ai-suggest:disabled{opacity:.7;cursor:wait}.btn-ai-suggest.btn-ai-claude{border-color:#d97706;background:linear-gradient(135deg, #FEF3C7, #FDE68A);color:#92400e}.btn-ai-suggest.btn-ai-claude:hover{background:linear-gradient(135deg, #D97706, #B45309);color:#fff;border-color:#b45309}.form_container{background:#fff;padding:25px;max-width:1300px;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.06)}.form_container.full{max-width:100%}.form_container .form_group{margin-bottom:12px;display:flex}.form_container .form_group>.label{width:300px;display:inline-flex;align-items:flex-start;justify-content:right;padding-right:12px}.form_container .form_group .input{width:calc(100% - 300px)}.default_popup{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.45);display:none;z-index:2000}.default_popup .popup_content{position:absolute;top:50%;left:50%;transform:translate(-50%, -50%);background:#fff;padding:25px;border-radius:10px;max-width:1140px;width:95%;box-shadow:0 20px 60px rgba(0,0,0,.15)}.default_popup .popup_content .popup_header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px}.default_popup .popup_content .popup_header .title{font-size:18px;font-weight:600}.default_popup .popup_content .close{cursor:pointer;color:#a0aec0;font-size:18px;padding:4px}.default_popup .popup_content .close:hover{color:#c00}.dt-layout-table{margin-bottom:20px}.pagination button{border:1px solid #e2e8f0;background:#fff;display:inline-flex;height:32px;width:32px;align-items:center;justify-content:center;margin:0 2px;border-radius:4px;transition:all .2s;cursor:pointer}.pagination button:hover{background:#f4f6f9;border-color:#6690f4}table#products a{color:inherit;text-decoration:none}table#products .table-product-title{display:flex;justify-content:space-between}table#products .edit-product-title{display:flex;height:25px;align-items:center;justify-content:center;width:25px;cursor:pointer;background:#fff;border:1px solid #cbd5e0;color:#cbd5e0;border-radius:4px}table#products .edit-product-title:hover{background:#cbd5e0;color:#fff}table#products a.custom_name{color:#57b951 !important}.product-history-page .product-history-meta{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}.product-history-page .product-history-meta span{display:inline-flex;align-items:center;padding:5px 10px;border-radius:999px;font-size:12px;font-weight:600;color:#4e5e6a;background:#eef2ff;border:1px solid #d9e2ff}.product-history-page .product-history-chart-wrap{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);padding:20px;margin-bottom:16px}.product-history-page .chart-with-form{display:flex;gap:20px;align-items:flex-start}.product-history-page .chart-area{flex:1 1 auto;min-width:0}.product-history-page .product-history-chart{min-height:360px}.product-history-page .comment-form{width:340px;flex:0 0 340px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:14px}.product-history-page .comment-form .form-group{margin-bottom:12px}.product-history-page .comment-form label{display:block;font-weight:600;margin-bottom:6px;font-size:13px;color:#52606d}.product-history-page .comment-form input[type=date],.product-history-page .comment-form textarea{width:100%;border:1px solid #e2e8f0;border-radius:6px;padding:8px 12px;font-size:14px;font-family:\"Roboto\",sans-serif;background:#fff}.product-history-page .comment-form input[type=date]:focus,.product-history-page .comment-form textarea:focus{outline:none;border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1)}.product-history-page .comment-form textarea{min-height:110px;resize:vertical}.product-history-page .comment-form .btn{width:100%;justify-content:center;padding:10px 16px}.product-history-page .comment-form .btn[disabled]{opacity:.6;cursor:not-allowed}.product-history-page .products-table-wrap{overflow-x:auto}.product-history-page .products-table-wrap .table{min-width:980px}.product-history-page .products-table-wrap .comment-cell{display:flex;align-items:center;justify-content:space-between;gap:10px}.product-history-page .products-table-wrap .comment-text{word-break:break-word}.product-history-page .products-table-wrap .delete-comment{color:#c00;text-decoration:none;font-weight:600;white-space:nowrap}.product-history-page .products-table-wrap .delete-comment:hover{text-decoration:underline}.product-history-page .products-table-wrap .dt-paging .pagination .page-item{list-style:none}.cron-status-overview{display:flex;flex-wrap:wrap;gap:10px 20px;margin-bottom:20px;color:#4e5e6a;font-size:13px}.cron-progress-list{margin-bottom:20px}.cron-schedule-list{margin-bottom:20px}.cron-schedule-item{border:1px solid #dfe7f0;background:#f4f8fd;border-radius:8px;padding:9px 12px;margin-bottom:8px}.cron-schedule-item:last-child{margin-bottom:0}.cron-schedule-item strong{display:block;color:#2d3748;font-size:13px;font-weight:700;margin-bottom:2px}.cron-schedule-item small{display:block;color:#678;font-size:12px;line-height:1.35}.cron-progress-item{margin-bottom:14px}.cron-progress-item:last-child{margin-bottom:0}.cron-progress-item .cron-progress-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:6px;font-size:13px}.cron-progress-item .cron-progress-head strong{color:#2d3748;font-weight:600}.cron-progress-item .cron-progress-head span{color:#6b7a89;font-size:12px;font-weight:600;white-space:nowrap}.cron-progress-item small{display:block;margin-top:5px;color:#789;font-size:12px}.cron-progress-bar{width:100%;height:10px;border-radius:999px;background:#e9eef5;overflow:hidden}.cron-progress-bar>span{display:block;height:100%;background:linear-gradient(90deg, #5A9BFF 0%, #2E6BDF 100%)}.cron-url-list{margin-bottom:20px}.cron-url-item{border:1px solid #e2e8f0;border-radius:8px;background:#f8fafc;padding:10px 12px;margin-bottom:10px}.cron-url-item:last-child{margin-bottom:0}.cron-url-item .cron-url-top{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:6px}.cron-url-item .cron-url-top strong{color:#2d3748;font-size:13px;font-weight:600}.cron-url-item .cron-url-top small{color:#7a8794;font-size:11px;white-space:nowrap}.cron-url-item code{display:block;background:#eef2f7;border:1px solid #dde4ed;border-radius:6px;padding:6px 8px;color:#2e3b49;font-size:12px;overflow-x:auto}.cron-url-item .cron-url-plan{display:block;color:#6c7b8a;font-size:11px;margin-bottom:6px}@media(max-width: 1200px){.product-history-page .chart-with-form{flex-direction:column}.product-history-page .comment-form{width:100%;flex:1 1 auto}}.jconfirm-box .form-group .select2-container,.adspro-dialog-box .form-group .select2-container{width:100% !important;margin-top:8px}.jconfirm-box .select2-container--default .select2-selection--single,.adspro-dialog-box .select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #e2e8f0;border-radius:6px;min-height:42px;display:flex;align-items:center;padding:4px 12px;box-shadow:none;transition:border-color .2s,box-shadow .2s;font-size:14px}.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__rendered,.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__rendered{padding-left:0;line-height:1.4;color:#495057}.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__placeholder,.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__placeholder{color:#cbd5e0}.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__arrow,.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__arrow{height:100%;right:8px}.jconfirm-box .select2-container--default.select2-container--focus .select2-selection--single,.jconfirm-box .select2-container--default .select2-selection--single:hover,.adspro-dialog-box .select2-container--default.select2-container--focus .select2-selection--single,.adspro-dialog-box .select2-container--default .select2-selection--single:hover{border-color:#6690f4;box-shadow:0 0 0 3px rgba(102,144,244,.1);outline:0}.jconfirm-box .select2-container .select2-dropdown,.adspro-dialog-box .select2-container .select2-dropdown{border-color:#e2e8f0;border-radius:0 0 6px 6px;font-size:14px}.jconfirm-box .select2-container .select2-search--dropdown .select2-search__field,.adspro-dialog-box .select2-container .select2-search--dropdown .select2-search__field{padding:6px 10px;border-radius:4px;border:1px solid #e2e8f0;font-size:14px}.jconfirm-box .select2-container--default .select2-results__option--highlighted[aria-selected],.adspro-dialog-box .select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#6690f4;color:#fff}@media(max-width: 992px){.sidebar{transform:translateX(-100%)}.sidebar.mobile-open{transform:translateX(0)}.main-wrapper{margin-left:0 !important}}.campaign-terms-wrap{display:flex;flex-direction:column;gap:20px;margin-top:20px}.campaign-terms-page{max-width:100%;overflow:hidden}.campaign-terms-page .campaigns-filters{flex-wrap:wrap}.campaign-terms-page .campaigns-filters .filter-group{min-width:220px}.campaign-terms-page .campaigns-filters .filter-group.terms-columns-group{min-width:280px}.campaign-terms-page .terms-card-toggle{margin-left:auto;width:28px;height:28px;border:1px solid #e2e8f0;border-radius:6px;background:#fff;color:#475569;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:all .2s}.campaign-terms-page .terms-card-toggle:hover{background:#f8fafc;border-color:#cbd5e1}.campaign-terms-page .terms-adgroups-card.is-collapsed .campaigns-extra-table-wrap{display:none}.campaign-terms-page .terms-search-toolbar{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid #eef2f7;background:#fff}.campaign-terms-page .terms-search-toolbar label{font-size:12px;font-weight:600;color:#475569;display:inline-flex;align-items:center;gap:6px;margin:0;white-space:nowrap}.campaign-terms-page .terms-search-toolbar .terms-search-toolbar-label{min-width:86px}.campaign-terms-page .terms-search-toolbar #terms_min_clicks_all,.campaign-terms-page .terms-search-toolbar #terms_max_clicks_all{width:160px;height:32px}.campaign-terms-page .terms-search-toolbar #terms_min_conversions_all,.campaign-terms-page .terms-search-toolbar #terms_max_conversions_all{width:130px;max-width:130px}.campaign-terms-page .terms-search-selected-label{margin:0;font-size:12px;color:#475569;font-weight:600;white-space:nowrap}.campaign-terms-page .terms-ai-analyze-btn{margin-left:auto;display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border-radius:6px;border:1px solid #bfdbfe;background:#eff6ff;color:#1d4ed8;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s}.campaign-terms-page .terms-ai-analyze-btn:hover{background:#dbeafe;border-color:#93c5fd}.campaign-terms-page .terms-ai-analyze-btn:disabled{opacity:.6;cursor:wait}.campaign-terms-page .terms-negative-toolbar{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid #eef2f7;background:#fff}.campaign-terms-page .terms-negative-bulk-btn{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border-radius:6px;border:1px solid #fecaca;background:#fef2f2;color:#dc2626;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s}.campaign-terms-page .terms-negative-bulk-btn:hover{background:#fee2e2;border-color:#fca5a5}.campaign-terms-page .terms-negative-bulk-btn:disabled{opacity:.5;cursor:not-allowed}.campaign-terms-page table.campaigns-extra-table>thead>tr>th{position:sticky;top:0;z-index:2;background-color:#111827 !important;color:#e5e7eb !important;border-bottom:1px solid #0b1220 !important;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;padding:10px 12px;white-space:nowrap}.campaign-terms-page #terms_search_table thead th .dt-column-order,.campaign-terms-page #terms_negative_table thead th .dt-column-order{display:none !important}.campaign-terms-page #terms_search_table thead th.dt-orderable-asc,.campaign-terms-page #terms_search_table thead th.dt-orderable-desc,.campaign-terms-page #terms_negative_table thead th.dt-orderable-asc,.campaign-terms-page #terms_negative_table thead th.dt-orderable-desc{cursor:pointer;padding-right:34px;overflow:hidden}.campaign-terms-page #terms_search_table thead th .dt-column-title,.campaign-terms-page #terms_negative_table thead th .dt-column-title{display:block;overflow:hidden;text-overflow:ellipsis;padding-right:2px}.campaign-terms-page #terms_search_table thead th.dt-orderable-asc::after,.campaign-terms-page #terms_search_table thead th.dt-orderable-desc::after,.campaign-terms-page #terms_negative_table thead th.dt-orderable-asc::after,.campaign-terms-page #terms_negative_table thead th.dt-orderable-desc::after{content:\"↕\";position:absolute;right:10px;top:50%;transform:translateY(-50%);width:16px;height:16px;border-radius:999px;font-size:12px;font-weight:700;line-height:16px;text-align:center;color:#e5e7eb;background:#374151}.campaign-terms-page #terms_search_table thead th.dt-ordering-asc::after,.campaign-terms-page #terms_negative_table thead th.dt-ordering-asc::after,.campaign-terms-page #terms_search_table thead th[aria-sort=ascending]::after,.campaign-terms-page #terms_negative_table thead th[aria-sort=ascending]::after{content:\"▲\";color:#fff;background:#2563eb}.campaign-terms-page #terms_search_table thead th.dt-ordering-desc::after,.campaign-terms-page #terms_negative_table thead th.dt-ordering-desc::after,.campaign-terms-page #terms_search_table thead th[aria-sort=descending]::after,.campaign-terms-page #terms_negative_table thead th[aria-sort=descending]::after{content:\"▼\";color:#fff;background:#2563eb}.campaign-terms-page #terms_negative_select_all,.campaign-terms-page .terms-negative-select-row,.campaign-terms-page #terms_search_select_all,.campaign-terms-page .terms-search-select-row{width:14px;height:14px;cursor:pointer}.campaign-terms-page .dt-layout-row:first-child{display:none}.campaign-terms-page .dt-layout-row{padding:10px 12px;margin:0 !important;border-top:1px solid #f1f5f9}.campaign-terms-page .dt-info{font-size:12px;color:#64748b}.campaign-terms-page .dt-paging .pagination{margin:0;padding:0;list-style:none !important;display:flex;align-items:center;gap:6px}.campaign-terms-page .dt-paging .pagination .page-item{list-style:none !important}.campaign-terms-page .dt-paging .pagination .page-item .page-link{display:inline-flex;align-items:center;justify-content:center;min-width:36px;width:fit-content;height:32px;padding:0 12px;border-radius:6px;font-size:12px;font-weight:500;border:1px solid #e2e8f0;background:#fff;color:#4e5e6a;text-decoration:none;line-height:1;white-space:nowrap}.campaign-terms-page .dt-paging .pagination .page-item .page-link:hover{background:#eef2ff;color:#6690f4;border-color:#6690f4}.campaign-terms-page .dt-paging .pagination .page-item.previous .page-link,.campaign-terms-page .dt-paging .pagination .page-item.next .page-link{min-width:72px}.campaign-terms-page .dt-paging .pagination .page-item.active .page-link{background:#6690f4;color:#fff;border-color:#6690f4}.campaign-terms-page .dt-paging .pagination .page-item.disabled .page-link{opacity:.35;cursor:default;pointer-events:none}.terms-columns-box{display:flex;flex-direction:column;gap:6px}.terms-columns-control{border:1px solid #e2e8f0;border-radius:6px;background:#fff;overflow:hidden}.terms-columns-control summary{cursor:pointer;padding:8px 10px;font-size:12px;font-weight:600;color:#334155;list-style:none}.terms-columns-control summary::-webkit-details-marker{display:none}.terms-columns-control summary::after{content:\"▼\";float:right;font-size:10px;color:#64748b;margin-top:2px}.terms-columns-control[open] summary::after{content:\"▲\"}.terms-columns-list{border-top:1px solid #eef2f7;padding:8px 10px;max-height:180px;overflow-y:auto}.terms-columns-list .terms-col-item{display:flex;align-items:center;gap:8px;font-size:12px;color:#334155;margin-bottom:6px}.terms-columns-list .terms-col-item:last-child{margin-bottom:0}.terms-columns-list .terms-col-item input[type=checkbox]{margin:0}.campaigns-extra-card{background:#fff;border-radius:10px;box-shadow:0 1px 4px rgba(0,0,0,.06);overflow:hidden}.campaigns-extra-card-title{padding:14px 16px;border-bottom:1px solid #e2e8f0;font-size:13px;font-weight:700;color:#334155;display:flex;align-items:center;gap:8px}.campaigns-extra-card-title .terms-card-title-label{display:inline-flex;align-items:center;gap:8px}.campaigns-extra-table-wrap{overflow:auto}.campaigns-extra-table{margin:0;width:100%;table-layout:fixed}.campaigns-extra-table tbody td{padding:9px 12px;border-bottom:1px solid #f1f5f9;font-size:13px;color:#334155;vertical-align:middle;white-space:nowrap}.campaigns-extra-table td.num-cell{text-align:right;white-space:nowrap}.campaigns-extra-table td.text-cell{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.campaigns-extra-table th.terms-negative-select-cell,.campaigns-extra-table td.terms-negative-select-cell,.campaigns-extra-table th.terms-search-select-cell,.campaigns-extra-table td.terms-search-select-cell{text-align:center}.campaigns-extra-table th.phrase-nowrap,.campaigns-extra-table td.phrase-nowrap{white-space:nowrap !important;overflow:hidden;text-overflow:ellipsis}.campaigns-extra-table .terms-add-negative-btn,.campaigns-extra-table .terms-remove-negative-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;cursor:pointer;transition:all .2s}.campaigns-extra-table .terms-add-negative-btn{border:1px solid #e2e8f0;background:#eef2ff;color:#3b82f6}.campaigns-extra-table .terms-add-negative-btn:hover{background:#3b82f6;color:#fff;border-color:#3b82f6}.campaigns-extra-table .terms-remove-negative-btn{border:1px solid #fecaca;background:#fef2f2;color:#dc2626}.campaigns-extra-table .terms-remove-negative-btn:hover{background:#dc2626;color:#fff;border-color:#dc2626}.campaigns-extra-table tbody tr:hover{background:#f8fafc}.campaigns-extra-table tbody tr.term-is-negative td{color:#dc2626}.campaigns-extra-table tbody tr.term-is-negative:hover{background:#fef2f2}.campaigns-empty-row{text-align:center;color:#94a3b8 !important;font-style:italic}.terms-ai-modal-toolbar{display:flex;align-items:center;gap:10px;margin-bottom:10px}.terms-ai-modal-toolbar label{font-size:12px;font-weight:600;color:#334155;margin:0}.terms-ai-modal-toolbar .form-control{width:200px;height:32px}.terms-ai-summary{font-size:12px;color:#64748b;margin-bottom:10px}.terms-ai-results-wrap{border:1px solid #e2e8f0;border-radius:8px;max-height:420px;overflow:auto}.terms-ai-results-table{width:100%;border-collapse:collapse;font-size:12px}.terms-ai-results-table th,.terms-ai-results-table td{border-bottom:1px solid #eef2f7;padding:8px;vertical-align:middle}.terms-ai-results-table th{position:sticky;top:0;background:#f8fafc;color:#334155;font-weight:700}.terms-ai-results-table td.term-col{min-width:260px;max-width:380px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.terms-ai-results-table td.reason-col{min-width:320px}.terms-ai-action-badge{display:inline-flex;align-items:center;justify-content:center;border-radius:999px;padding:2px 8px;font-size:11px;font-weight:700}.terms-ai-action-badge.action-exclude{background:#fee2e2;color:#b91c1c}.terms-ai-action-badge.action-keep{background:#dcfce7;color:#166534}.products-page .products-filters .filter-group.filter-group-columns{min-width:240px}.products-columns-control{border:1px solid #e2e8f0;border-radius:6px;background:#fff;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:\"▼\";float:right;font-size:10px;color:#64748b;margin-top:2px}.products-columns-control[open] summary::after{content:\"▲\"}.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}#products th:last-child,#products td:last-child{white-space:nowrap}#products .products-row-actions{display:inline-flex;align-items:center;gap:4px}#products .products-row-actions .btn{width:38px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;border-radius:4px !important}#products .products-row-actions .btn i{line-height:1}.products-page table#products>thead>tr>th{position:sticky;top:0;z-index:2;background-color:#111827 !important;color:#e5e7eb !important;border-bottom:1px solid #0b1220 !important;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;padding:10px 12px;white-space:nowrap}.products-page #products thead th .dt-column-order{display:none !important}.products-page #products thead th.dt-orderable-asc,.products-page #products thead th.dt-orderable-desc{cursor:pointer;padding-right:34px;overflow:hidden}.products-page #products thead th .dt-column-title{display:block;overflow:hidden;text-overflow:ellipsis;padding-right:2px}.products-page #products thead th.dt-orderable-asc::after,.products-page #products thead th.dt-orderable-desc::after{content:\"↕\";position:absolute;right:10px;top:50%;transform:translateY(-50%);width:16px;height:16px;border-radius:999px;font-size:12px;font-weight:700;line-height:16px;text-align:center;color:#e5e7eb;background:#374151}.products-page #products thead th.dt-ordering-asc::after,.products-page #products thead th[aria-sort=ascending]::after{content:\"▲\";color:#fff;background:#2563eb}.products-page #products thead th.dt-ordering-desc::after,.products-page #products thead th[aria-sort=descending]::after{content:\"▼\";color:#fff;background:#2563eb}","@use \"sass:color\";\r\n// === adsPRO - Nowe style ===\r\n\r\n// --- Zmienne ---\r\n$cPrimary: #6690F4;\r\n$cPrimaryDark: #3164db;\r\n$cSidebarBg: #1E2A3A;\r\n$cSidebarText: #A8B7C7;\r\n$cSidebarHover: #263548;\r\n$cSidebarActive: $cPrimary;\r\n$cContentBg: #F4F6F9;\r\n$cWhite: #FFFFFF;\r\n$cText: #4E5E6A;\r\n$cTextDark: #2D3748;\r\n$cBorder: #E2E8F0;\r\n$cSuccess: #57B951;\r\n$cSuccessDark: #4a9c3b;\r\n$cDanger: #CC0000;\r\n$cDangerDark: #b30000;\r\n$cWarning: #FF8C00;\r\n$cGreenLight: #57b951;\r\n\r\n$sidebarWidth: 260px;\r\n$sidebarCollapsed: 70px;\r\n$topbarHeight: 56px;\r\n$transitionSpeed: 0.3s;\r\n\r\n// --- Reset i baza ---\r\n* {\r\n box-sizing: border-box;\r\n}\r\n\r\nbody {\r\n font-family: \"Roboto\", sans-serif;\r\n margin: 0;\r\n padding: 0;\r\n font-size: 14px;\r\n color: $cText;\r\n background: $cContentBg;\r\n max-width: 100vw;\r\n overflow-x: hidden;\r\n}\r\n\r\n.hide {\r\n display: none;\r\n}\r\n\r\n// --- Typografia ---\r\nsmall {\r\n font-size: .75em;\r\n}\r\n\r\n.text-right {\r\n text-align: right;\r\n}\r\n\r\n.text-bold {\r\n font-weight: 700 !important;\r\n}\r\n\r\n.nowrap {\r\n white-space: nowrap;\r\n}\r\n\r\n// ===========================\r\n// LOGIN PAGE (unlogged)\r\n// ===========================\r\nbody.unlogged {\r\n background: $cContentBg;\r\n margin: 0;\r\n padding: 0;\r\n}\r\n\r\n.login-container {\r\n display: flex;\r\n min-height: 100vh;\r\n}\r\n\r\n.login-brand {\r\n flex: 0 0 45%;\r\n background: linear-gradient(135deg, $cSidebarBg 0%, #2C3E57 50%, $cPrimary 100%);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 60px;\r\n position: relative;\r\n overflow: hidden;\r\n\r\n &::before {\r\n content: '';\r\n position: absolute;\r\n top: -50%;\r\n right: -50%;\r\n width: 100%;\r\n height: 100%;\r\n background: radial-gradient(circle, rgba($cPrimary, 0.15) 0%, transparent 70%);\r\n border-radius: 50%;\r\n }\r\n\r\n .brand-content {\r\n position: relative;\r\n z-index: 1;\r\n color: $cWhite;\r\n max-width: 400px;\r\n }\r\n\r\n .brand-logo {\r\n font-size: 48px;\r\n font-weight: 300;\r\n margin-bottom: 20px;\r\n letter-spacing: -1px;\r\n\r\n strong {\r\n font-weight: 700;\r\n }\r\n }\r\n\r\n .brand-tagline {\r\n font-size: 18px;\r\n opacity: 0.85;\r\n line-height: 1.6;\r\n margin-bottom: 50px;\r\n }\r\n\r\n .brand-features {\r\n .feature {\r\n display: flex;\r\n align-items: center;\r\n gap: 15px;\r\n margin-bottom: 20px;\r\n opacity: 0.8;\r\n\r\n i {\r\n font-size: 20px;\r\n width: 40px;\r\n height: 40px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n background: rgba($cWhite, 0.1);\r\n border-radius: 10px;\r\n }\r\n\r\n span {\r\n font-size: 15px;\r\n }\r\n }\r\n }\r\n}\r\n\r\n.login-form-wrapper {\r\n flex: 1;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n padding: 60px;\r\n background: $cWhite;\r\n}\r\n\r\n.login-box {\r\n width: 100%;\r\n max-width: 420px;\r\n\r\n .login-header {\r\n margin-bottom: 35px;\r\n\r\n h1 {\r\n font-size: 28px;\r\n font-weight: 700;\r\n color: $cTextDark;\r\n margin: 0 0 8px;\r\n }\r\n\r\n p {\r\n color: #718096;\r\n font-size: 15px;\r\n margin: 0;\r\n }\r\n }\r\n\r\n .form-group {\r\n margin-bottom: 20px;\r\n\r\n label {\r\n display: block;\r\n font-size: 13px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n margin-bottom: 6px;\r\n }\r\n }\r\n\r\n .input-with-icon {\r\n position: relative;\r\n\r\n i {\r\n position: absolute;\r\n left: 14px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n color: #A0AEC0;\r\n font-size: 14px;\r\n }\r\n\r\n .form-control {\r\n padding-left: 42px;\r\n }\r\n }\r\n\r\n .form-control {\r\n width: 100%;\r\n height: 46px;\r\n border: 2px solid $cBorder;\r\n border-radius: 8px;\r\n padding: 0 14px;\r\n font-size: 14px;\r\n font-family: \"Roboto\", sans-serif;\r\n color: $cTextDark;\r\n transition: border-color $transitionSpeed, box-shadow $transitionSpeed;\r\n\r\n &::placeholder {\r\n color: #CBD5E0;\r\n }\r\n\r\n &:focus {\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.15);\r\n outline: none;\r\n }\r\n }\r\n\r\n .form-error {\r\n color: $cDanger;\r\n font-size: 12px;\r\n margin-top: 4px;\r\n }\r\n\r\n .checkbox-group {\r\n .checkbox-label {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n cursor: pointer;\r\n font-size: 13px;\r\n color: #718096;\r\n font-weight: 400;\r\n\r\n input[type=\"checkbox\"] {\r\n width: 16px;\r\n height: 16px;\r\n accent-color: $cPrimary;\r\n }\r\n }\r\n }\r\n\r\n .btn-login {\r\n width: 100%;\r\n height: 48px;\r\n font-size: 15px;\r\n font-weight: 600;\r\n border-radius: 8px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 8px;\r\n\r\n &.disabled {\r\n opacity: 0.7;\r\n pointer-events: none;\r\n }\r\n }\r\n\r\n .alert {\r\n display: none;\r\n padding: 12px 16px;\r\n border-radius: 8px;\r\n font-size: 13px;\r\n margin-bottom: 20px;\r\n\r\n &.alert-danger {\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n border: 1px solid #FED7D7;\r\n }\r\n\r\n &.alert-success {\r\n background: #F0FFF4;\r\n color: #276749;\r\n border: 1px solid #C6F6D5;\r\n }\r\n }\r\n}\r\n\r\n// Responsywność logowania\r\n@media (max-width: 768px) {\r\n .login-brand {\r\n display: none;\r\n }\r\n\r\n .login-form-wrapper {\r\n padding: 30px 20px;\r\n }\r\n}\r\n\r\n// ===========================\r\n// LAYOUT (logged) - SIDEBAR\r\n// ===========================\r\nbody.logged {\r\n display: flex;\r\n min-height: 100vh;\r\n background: $cContentBg;\r\n}\r\n\r\n// --- Sidebar ---\r\n.sidebar {\r\n width: $sidebarWidth;\r\n min-height: 100vh;\r\n background: $cSidebarBg;\r\n position: fixed;\r\n top: 0;\r\n left: 0;\r\n z-index: 1000;\r\n display: flex;\r\n flex-direction: column;\r\n transition: width $transitionSpeed ease;\r\n overflow: hidden;\r\n\r\n &.collapsed {\r\n width: $sidebarCollapsed;\r\n\r\n .sidebar-header {\r\n padding: 16px 0;\r\n justify-content: center;\r\n\r\n .sidebar-logo {\r\n display: none;\r\n }\r\n\r\n .sidebar-toggle i {\r\n transform: rotate(180deg);\r\n }\r\n }\r\n\r\n .sidebar-nav ul li a {\r\n padding: 12px 0;\r\n justify-content: center;\r\n\r\n span {\r\n display: none;\r\n }\r\n\r\n i {\r\n margin-right: 0;\r\n font-size: 18px;\r\n }\r\n }\r\n\r\n .sidebar-nav ul li.nav-group .nav-group-label {\r\n padding: 12px 0;\r\n justify-content: center;\r\n\r\n span {\r\n display: none;\r\n }\r\n\r\n i {\r\n margin-right: 0;\r\n font-size: 18px;\r\n }\r\n }\r\n\r\n .sidebar-footer {\r\n .sidebar-user {\r\n justify-content: center;\r\n\r\n .user-info {\r\n display: none;\r\n }\r\n }\r\n\r\n .sidebar-logout {\r\n justify-content: center;\r\n\r\n span {\r\n display: none;\r\n }\r\n }\r\n }\r\n\r\n .nav-divider {\r\n margin: 8px 15px;\r\n }\r\n }\r\n}\r\n\r\n.sidebar-header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 20px 20px 16px;\r\n border-bottom: 1px solid rgba($cWhite, 0.08);\r\n\r\n .sidebar-logo a {\r\n color: $cWhite;\r\n text-decoration: none;\r\n font-size: 24px;\r\n font-weight: 300;\r\n letter-spacing: -0.5px;\r\n\r\n strong {\r\n font-weight: 700;\r\n }\r\n }\r\n\r\n .sidebar-toggle {\r\n background: none;\r\n border: none;\r\n color: $cSidebarText;\r\n cursor: pointer;\r\n padding: 6px;\r\n border-radius: 6px;\r\n transition: all $transitionSpeed;\r\n\r\n &:hover {\r\n background: rgba($cWhite, 0.08);\r\n color: $cWhite;\r\n }\r\n\r\n i {\r\n transition: transform $transitionSpeed;\r\n }\r\n }\r\n}\r\n\r\n.sidebar-nav {\r\n flex: 1;\r\n padding: 12px 0;\r\n overflow-y: auto;\r\n\r\n ul {\r\n list-style: none;\r\n margin: 0;\r\n padding: 0;\r\n\r\n li {\r\n &.nav-group {\r\n margin-bottom: 4px;\r\n\r\n .nav-group-label {\r\n display: flex;\r\n align-items: center;\r\n padding: 11px 20px;\r\n color: #D5DEEA;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.6px;\r\n border-left: 3px solid transparent;\r\n\r\n i {\r\n width: 20px;\r\n text-align: center;\r\n margin-right: 12px;\r\n font-size: 14px;\r\n color: #B6C4D3;\r\n }\r\n }\r\n\r\n .nav-submenu {\r\n margin: 0;\r\n padding: 0;\r\n list-style: none;\r\n\r\n li a {\r\n padding-left: 44px;\r\n }\r\n }\r\n\r\n &.active>.nav-group-label {\r\n color: $cWhite;\r\n background: rgba($cPrimary, 0.12);\r\n border-left-color: $cPrimary;\r\n\r\n i {\r\n color: $cPrimary;\r\n }\r\n }\r\n }\r\n\r\n &.nav-divider {\r\n height: 1px;\r\n background: rgba($cWhite, 0.08);\r\n margin: 8px 20px;\r\n }\r\n\r\n a {\r\n display: flex;\r\n align-items: center;\r\n padding: 11px 20px;\r\n color: $cSidebarText;\r\n text-decoration: none;\r\n font-size: 14px;\r\n transition: all 0.2s;\r\n border-left: 3px solid transparent;\r\n\r\n i {\r\n width: 20px;\r\n text-align: center;\r\n margin-right: 12px;\r\n font-size: 15px;\r\n }\r\n\r\n &:hover {\r\n background: $cSidebarHover;\r\n color: $cWhite;\r\n }\r\n }\r\n\r\n &.active>a {\r\n background: rgba($cPrimary, 0.15);\r\n color: $cWhite;\r\n border-left-color: $cPrimary;\r\n\r\n i {\r\n color: $cPrimary;\r\n }\r\n }\r\n }\r\n }\r\n}\r\n\r\n.badge-alerts-count {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n min-width: 20px;\r\n height: 20px;\r\n padding: 0 6px;\r\n margin-left: 8px;\r\n border-radius: 50%;\r\n font-size: 11px;\r\n font-weight: 600;\r\n line-height: 1;\r\n background: $cWhite;\r\n color: $cPrimary;\r\n}\r\n\r\n.sidebar-footer {\r\n padding: 16px 20px;\r\n border-top: 1px solid rgba($cWhite, 0.08);\r\n\r\n .sidebar-user {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n margin-bottom: 12px;\r\n\r\n .user-avatar {\r\n width: 34px;\r\n height: 34px;\r\n border-radius: 50%;\r\n background: rgba($cPrimary, 0.2);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n color: $cPrimary;\r\n font-size: 14px;\r\n flex-shrink: 0;\r\n }\r\n\r\n .user-info {\r\n overflow: hidden;\r\n\r\n .user-email {\r\n color: $cSidebarText;\r\n font-size: 12px;\r\n display: block;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n }\r\n }\r\n }\r\n\r\n .sidebar-logout {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n color: #E53E3E;\r\n text-decoration: none;\r\n font-size: 13px;\r\n padding: 8px 10px;\r\n border-radius: 6px;\r\n transition: all 0.2s;\r\n\r\n i {\r\n font-size: 14px;\r\n }\r\n\r\n &:hover {\r\n background: rgba(#E53E3E, 0.1);\r\n }\r\n }\r\n}\r\n\r\n// --- Main wrapper ---\r\n.main-wrapper {\r\n margin-left: $sidebarWidth;\r\n flex: 1;\r\n min-height: 100vh;\r\n transition: margin-left $transitionSpeed ease;\r\n display: flex;\r\n flex-direction: column;\r\n\r\n &.expanded {\r\n margin-left: $sidebarCollapsed;\r\n }\r\n}\r\n\r\n// --- Topbar ---\r\n.topbar {\r\n height: $topbarHeight;\r\n background: $cWhite;\r\n border-bottom: 1px solid $cBorder;\r\n display: flex;\r\n align-items: center;\r\n padding: 0 25px;\r\n position: sticky;\r\n top: 0;\r\n z-index: 500;\r\n\r\n .topbar-toggle {\r\n background: none;\r\n border: none;\r\n color: $cText;\r\n cursor: pointer;\r\n padding: 8px 10px;\r\n border-radius: 6px;\r\n font-size: 16px;\r\n margin-right: 15px;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: $cContentBg;\r\n }\r\n }\r\n\r\n .topbar-breadcrumb {\r\n font-size: 16px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n }\r\n}\r\n\r\n// --- Content area ---\r\n.content {\r\n flex: 1;\r\n padding: 25px;\r\n}\r\n\r\n.app-alert {\r\n background: #EBF8FF;\r\n border: 1px solid #BEE3F8;\r\n color: #2B6CB0;\r\n padding: 12px 16px;\r\n border-radius: 8px;\r\n margin-bottom: 20px;\r\n font-size: 14px;\r\n}\r\n\r\n// ===========================\r\n// KOMPONENTY WSPÓLNE\r\n// ===========================\r\n\r\n// --- Buttons ---\r\n.btn {\r\n padding: 10px 20px;\r\n transition: all 0.2s ease;\r\n color: $cWhite;\r\n border: 0;\r\n border-radius: 6px;\r\n cursor: pointer;\r\n display: inline-flex;\r\n text-decoration: none;\r\n gap: 6px;\r\n justify-content: center;\r\n align-items: center;\r\n font-size: 14px;\r\n font-family: \"Roboto\", sans-serif;\r\n font-weight: 500;\r\n\r\n &.btn_small,\r\n &.btn-xs,\r\n &.btn-sm {\r\n padding: 5px 10px;\r\n font-size: 12px;\r\n\r\n i {\r\n font-size: 11px;\r\n }\r\n }\r\n\r\n &.btn-success {\r\n background: $cSuccess;\r\n\r\n &:hover {\r\n background: $cSuccessDark;\r\n }\r\n }\r\n\r\n &.btn-primary {\r\n background: $cPrimary;\r\n\r\n &:hover {\r\n background: $cPrimaryDark;\r\n }\r\n }\r\n\r\n &.btn-danger {\r\n background: $cDanger;\r\n\r\n &:hover {\r\n background: $cDangerDark;\r\n }\r\n }\r\n\r\n &.disabled {\r\n opacity: 0.6;\r\n pointer-events: none;\r\n }\r\n}\r\n\r\n// --- Form controls ---\r\n.form-control {\r\n border: 1px solid $cBorder;\r\n border-radius: 6px;\r\n height: 38px;\r\n width: 100%;\r\n padding: 6px 12px;\r\n font-family: \"Roboto\", sans-serif;\r\n font-size: 14px;\r\n color: $cTextDark;\r\n transition: border-color 0.2s, box-shadow 0.2s;\r\n\r\n option {\r\n padding: 5px;\r\n }\r\n\r\n &:focus {\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);\r\n outline: none;\r\n }\r\n}\r\n\r\ninput[type=\"checkbox\"] {\r\n border: 1px solid $cBorder;\r\n}\r\n\r\n// --- Tables ---\r\ntable {\r\n border-collapse: collapse;\r\n font-size: 13px;\r\n}\r\n\r\n.table {\r\n width: 100%;\r\n\r\n th,\r\n td {\r\n border: 1px solid $cBorder;\r\n padding: 8px 10px;\r\n }\r\n\r\n th {\r\n background: #F7FAFC;\r\n font-weight: 600;\r\n font-size: 12px;\r\n text-transform: uppercase;\r\n letter-spacing: 0.03em;\r\n color: #718096;\r\n }\r\n\r\n td.center {\r\n text-align: center;\r\n }\r\n\r\n td.left {\r\n text-align: left;\r\n }\r\n\r\n &.table-sm td {\r\n padding: 5px !important;\r\n }\r\n\r\n input.form-control {\r\n font-size: 13px;\r\n height: 32px;\r\n }\r\n}\r\n\r\n// --- Cards ---\r\n.card {\r\n background: $cWhite;\r\n padding: 20px;\r\n border-radius: 8px;\r\n color: $cTextDark;\r\n font-size: 14px;\r\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);\r\n\r\n &.mb25 {\r\n margin-bottom: 20px;\r\n }\r\n\r\n .card-header {\r\n font-weight: 600;\r\n font-size: 15px;\r\n }\r\n\r\n .card-body {\r\n padding-top: 12px;\r\n\r\n table {\r\n\r\n th,\r\n td {\r\n font-size: 13px;\r\n\r\n &.bold {\r\n font-weight: 600;\r\n }\r\n\r\n &.text-right {\r\n text-align: right;\r\n }\r\n\r\n &.text-center {\r\n text-align: center;\r\n }\r\n }\r\n }\r\n }\r\n}\r\n\r\n// --- Action menu ---\r\n.action_menu {\r\n display: flex;\r\n margin-bottom: 20px;\r\n gap: 12px;\r\n\r\n .btn {\r\n padding: 8px 16px;\r\n\r\n &.btn_add {\r\n background: $cSuccess;\r\n\r\n &:hover {\r\n background: $cSuccessDark;\r\n }\r\n }\r\n\r\n &.btn_cancel {\r\n background: $cDanger;\r\n\r\n &:hover {\r\n background: $cDangerDark;\r\n }\r\n }\r\n }\r\n}\r\n\r\n// --- Settings page ---\r\n.settings-tabs {\r\n display: flex;\r\n gap: 8px;\r\n margin-bottom: 18px;\r\n\r\n .settings-tab {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n padding: 8px 14px;\r\n border-radius: 8px;\r\n text-decoration: none;\r\n color: #6B7A89;\r\n background: #E9EEF5;\r\n border: 1px solid #D8E0EA;\r\n font-size: 13px;\r\n font-weight: 600;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n color: $cTextDark;\r\n background: #DDE6F2;\r\n }\r\n\r\n &.active {\r\n color: $cWhite;\r\n background: $cPrimary;\r\n border-color: $cPrimary;\r\n }\r\n }\r\n}\r\n\r\n.settings-card {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n padding: 28px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n\r\n .settings-card-header {\r\n display: flex;\r\n align-items: center;\r\n gap: 14px;\r\n margin-bottom: 24px;\r\n padding-bottom: 16px;\r\n border-bottom: 1px solid $cBorder;\r\n\r\n .settings-card-icon {\r\n width: 44px;\r\n height: 44px;\r\n border-radius: 10px;\r\n background: color.adjust($cPrimary, $lightness: 26%);\r\n color: $cPrimary;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 18px;\r\n flex-shrink: 0;\r\n }\r\n\r\n h3 {\r\n margin: 0;\r\n font-size: 17px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n }\r\n\r\n small {\r\n color: #8899A6;\r\n font-size: 13px;\r\n }\r\n }\r\n\r\n .settings-field {\r\n margin-bottom: 18px;\r\n\r\n label {\r\n display: block;\r\n font-size: 13px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n margin-bottom: 6px;\r\n }\r\n }\r\n\r\n .settings-input-wrap {\r\n position: relative;\r\n\r\n .settings-input-icon {\r\n position: absolute;\r\n left: 12px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n color: #A0AEC0;\r\n font-size: 14px;\r\n pointer-events: none;\r\n }\r\n\r\n .form-control {\r\n padding-left: 38px;\r\n }\r\n\r\n .settings-toggle-pw {\r\n position: absolute;\r\n right: 4px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n background: none;\r\n border: none;\r\n color: #A0AEC0;\r\n cursor: pointer;\r\n padding: 6px 10px;\r\n font-size: 14px;\r\n transition: color 0.2s;\r\n\r\n &:hover {\r\n color: $cPrimary;\r\n }\r\n }\r\n }\r\n\r\n .settings-field .settings-toggle-label {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 10px;\r\n cursor: pointer;\r\n font-size: 14px;\r\n font-weight: 500;\r\n user-select: none;\r\n margin-bottom: 0;\r\n width: 100%;\r\n\r\n .settings-toggle-text {\r\n flex: 1 1 auto;\r\n min-width: 0;\r\n line-height: 1.35;\r\n }\r\n }\r\n\r\n .settings-toggle-checkbox {\r\n display: none;\r\n\r\n &+.settings-toggle-switch {\r\n display: inline-block;\r\n position: relative;\r\n width: 44px;\r\n height: 24px;\r\n background: #ccc;\r\n border-radius: 12px;\r\n transition: background 0.2s;\r\n flex-shrink: 0;\r\n\r\n &::after {\r\n content: '';\r\n position: absolute;\r\n top: 3px;\r\n left: 3px;\r\n width: 18px;\r\n height: 18px;\r\n background: #fff;\r\n border-radius: 50%;\r\n transition: transform 0.2s;\r\n }\r\n }\r\n\r\n &:checked+.settings-toggle-switch {\r\n background: #22C55E;\r\n\r\n &::after {\r\n transform: translateX(20px);\r\n }\r\n }\r\n }\r\n\r\n .settings-fields-grid {\r\n display: grid;\r\n grid-template-columns: 1fr 1fr;\r\n gap: 0 24px;\r\n\r\n @media (max-width: 768px) {\r\n grid-template-columns: 1fr;\r\n }\r\n }\r\n\r\n .settings-alert-error {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n border: 1px solid #FED7D7;\r\n border-radius: 8px;\r\n padding: 12px 16px;\r\n margin-bottom: 20px;\r\n font-size: 13px;\r\n\r\n i {\r\n font-size: 16px;\r\n flex-shrink: 0;\r\n }\r\n }\r\n}\r\n\r\n// --- Clients page ---\r\n.clients-page {\r\n .clients-header {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 20px;\r\n\r\n h2 {\r\n margin: 0;\r\n font-size: 20px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n\r\n i {\r\n color: $cPrimary;\r\n margin-right: 8px;\r\n }\r\n }\r\n }\r\n\r\n .clients-table-wrap {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n overflow: hidden;\r\n\r\n .table {\r\n margin: 0;\r\n\r\n thead th {\r\n background: #F8FAFC;\r\n border-bottom: 2px solid $cBorder;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.5px;\r\n color: #8899A6;\r\n padding: 14px 20px;\r\n }\r\n\r\n tbody td {\r\n padding: 14px 20px;\r\n vertical-align: middle;\r\n border-bottom: 1px solid #F1F5F9;\r\n }\r\n\r\n tbody tr:hover {\r\n background: #F8FAFC;\r\n }\r\n\r\n .client-id {\r\n color: #8899A6;\r\n font-size: 13px;\r\n font-weight: 600;\r\n }\r\n\r\n .client-name {\r\n font-weight: 600;\r\n color: $cTextDark;\r\n }\r\n }\r\n }\r\n\r\n .badge-id {\r\n display: inline-block;\r\n background: #EEF2FF;\r\n color: $cPrimary;\r\n font-size: 13px;\r\n font-weight: 600;\r\n padding: 4px 10px;\r\n border-radius: 6px;\r\n font-family: monospace;\r\n }\r\n\r\n .actions-cell {\r\n text-align: center;\r\n white-space: nowrap;\r\n }\r\n\r\n .btn-icon {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 34px;\r\n height: 34px;\r\n border-radius: 8px;\r\n border: none;\r\n cursor: pointer;\r\n font-size: 14px;\r\n transition: all 0.2s;\r\n margin: 0 2px;\r\n\r\n &.btn-icon-edit {\r\n background: #EEF2FF;\r\n color: $cPrimary;\r\n\r\n &:hover {\r\n background: $cPrimary;\r\n color: $cWhite;\r\n }\r\n }\r\n\r\n &.btn-icon-delete {\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n\r\n &:hover {\r\n background: $cDanger;\r\n color: $cWhite;\r\n }\r\n }\r\n\r\n &.btn-icon-sync {\r\n background: #F0FDF4;\r\n color: #16a34a;\r\n\r\n &:hover {\r\n background: #16a34a;\r\n color: $cWhite;\r\n }\r\n\r\n &:disabled {\r\n opacity: 0.7;\r\n cursor: wait;\r\n }\r\n\r\n &.is-queued {\r\n background: #FEF3C7;\r\n color: #D97706;\r\n }\r\n }\r\n }\r\n\r\n .client-sync-bars {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 4px;\r\n }\r\n\r\n .client-sync-row {\r\n display: flex;\r\n align-items: center;\r\n gap: 4px;\r\n }\r\n\r\n .client-sync-label {\r\n font-size: 11px;\r\n font-weight: 600;\r\n color: #8899A6;\r\n width: 18px;\r\n flex-shrink: 0;\r\n }\r\n\r\n .client-sync-track {\r\n flex: 1;\r\n height: 6px;\r\n border-radius: 999px;\r\n background: #E9EEF5;\r\n overflow: hidden;\r\n }\r\n\r\n .client-sync-fill {\r\n height: 100%;\r\n border-radius: 999px;\r\n background: #CBD5E0;\r\n transition: width 0.4s ease;\r\n\r\n &.is-active {\r\n background: linear-gradient(90deg, #5A9BFF 0%, #2E6BDF 100%);\r\n }\r\n\r\n &.is-done {\r\n background: $cSuccess;\r\n }\r\n }\r\n\r\n .client-sync-pct {\r\n font-size: 11px;\r\n font-weight: 600;\r\n color: #8899A6;\r\n width: 32px;\r\n text-align: right;\r\n flex-shrink: 0;\r\n }\r\n\r\n .empty-state {\r\n text-align: center;\r\n padding: 50px 20px !important;\r\n color: #A0AEC0;\r\n\r\n i {\r\n font-size: 40px;\r\n margin-bottom: 12px;\r\n display: block;\r\n }\r\n\r\n p {\r\n margin: 0;\r\n font-size: 15px;\r\n }\r\n }\r\n}\r\n\r\n.btn-secondary {\r\n background: #E2E8F0;\r\n color: $cTextDark;\r\n border: none;\r\n padding: 8px 18px;\r\n border-radius: 6px;\r\n font-size: 14px;\r\n cursor: pointer;\r\n transition: background 0.2s;\r\n\r\n &:hover {\r\n background: #CBD5E0;\r\n }\r\n}\r\n\r\n// ===========================\r\n// CAMPAIGNS PAGE\r\n// ===========================\r\n.campaigns-page {\r\n max-width: 100%;\r\n overflow-x: hidden;\r\n width: 100%;\r\n\r\n .campaigns-header {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 20px;\r\n\r\n h2 {\r\n margin: 0;\r\n font-size: 20px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n\r\n i {\r\n color: $cPrimary;\r\n margin-right: 8px;\r\n }\r\n }\r\n }\r\n\r\n .campaigns-filters {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 20px;\r\n margin-bottom: 20px;\r\n\r\n .filter-group {\r\n flex: 1;\r\n min-width: 0;\r\n\r\n label {\r\n display: block;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.5px;\r\n color: #8899A6;\r\n margin-bottom: 6px;\r\n\r\n i {\r\n margin-right: 4px;\r\n }\r\n }\r\n\r\n .form-control {\r\n width: 100%;\r\n padding: 10px 14px;\r\n border: 1px solid $cBorder;\r\n border-radius: 8px;\r\n font-size: 14px;\r\n color: $cTextDark;\r\n background: $cWhite;\r\n transition: border-color 0.2s;\r\n appearance: none;\r\n -webkit-appearance: none;\r\n 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\");\r\n background-repeat: no-repeat;\r\n background-position: right 12px center;\r\n padding-right: 32px;\r\n\r\n &:focus {\r\n outline: none;\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);\r\n }\r\n }\r\n\r\n .filter-with-action {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n\r\n .form-control {\r\n flex: 1;\r\n }\r\n\r\n .btn-icon {\r\n flex-shrink: 0;\r\n width: 42px;\r\n height: 42px;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border-radius: 8px;\r\n border: none;\r\n cursor: pointer;\r\n font-size: 14px;\r\n transition: all 0.2s;\r\n\r\n &.btn-icon-delete {\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n\r\n &:hover {\r\n background: $cDanger;\r\n color: $cWhite;\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n .filter-group-campaign-multi {\r\n flex: 2 !important;\r\n }\r\n\r\n .campaign-dropdown {\r\n flex: 1;\r\n min-width: 0;\r\n position: relative;\r\n }\r\n\r\n .campaign-dropdown-trigger {\r\n width: 100%;\r\n padding: 10px 14px;\r\n padding-right: 32px;\r\n border: 1px solid $cBorder;\r\n border-radius: 8px;\r\n font-size: 14px;\r\n color: $cTextDark;\r\n background: $cWhite;\r\n cursor: pointer;\r\n display: flex;\r\n align-items: center;\r\n transition: border-color 0.2s;\r\n position: relative;\r\n min-height: 42px;\r\n box-sizing: border-box;\r\n\r\n .campaign-dropdown-text {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n\r\n &.is-placeholder {\r\n color: #8899A6;\r\n }\r\n }\r\n\r\n .campaign-dropdown-arrow {\r\n position: absolute;\r\n right: 12px;\r\n font-size: 10px;\r\n color: #8899A6;\r\n transition: transform 0.2s;\r\n }\r\n }\r\n\r\n .campaign-dropdown.is-open {\r\n .campaign-dropdown-trigger {\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);\r\n }\r\n\r\n .campaign-dropdown-arrow {\r\n transform: rotate(180deg);\r\n }\r\n\r\n .campaign-dropdown-menu {\r\n display: block;\r\n }\r\n }\r\n\r\n .campaign-dropdown-menu {\r\n display: none;\r\n position: absolute;\r\n top: calc(100% + 4px);\r\n left: 0;\r\n right: 0;\r\n z-index: 100;\r\n max-height: 280px;\r\n overflow-y: auto;\r\n background: $cWhite;\r\n border: 1px solid $cBorder;\r\n border-radius: 8px;\r\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\r\n padding: 4px 0;\r\n }\r\n\r\n .campaign-dropdown-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 8px 12px;\r\n cursor: pointer;\r\n font-size: 14px;\r\n color: $cTextDark;\r\n margin: 0;\r\n transition: background 0.15s;\r\n\r\n &:hover {\r\n background: #F8FAFC;\r\n }\r\n\r\n &.is-checked {\r\n background: #EEF2FF;\r\n }\r\n\r\n input[type=\"checkbox\"] {\r\n width: 16px;\r\n height: 16px;\r\n cursor: pointer;\r\n flex-shrink: 0;\r\n accent-color: $cPrimary;\r\n }\r\n\r\n span {\r\n flex: 1;\r\n min-width: 0;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n }\r\n }\r\n }\r\n\r\n .campaigns-list-panel {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n margin-bottom: 20px;\r\n overflow: hidden;\r\n\r\n .campaigns-list-toolbar {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 12px 16px;\r\n border-bottom: 1px solid $cBorder;\r\n gap: 12px;\r\n\r\n &-left {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n font-size: 13px;\r\n color: $cText;\r\n\r\n input[type=\"checkbox\"] {\r\n width: 16px;\r\n height: 16px;\r\n cursor: pointer;\r\n }\r\n\r\n label {\r\n cursor: pointer;\r\n user-select: none;\r\n margin: 0;\r\n }\r\n\r\n .campaigns-selected-count {\r\n margin-left: 12px;\r\n color: #8899A6;\r\n }\r\n }\r\n\r\n &-right {\r\n .campaigns-bulk-delete-btn {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n padding: 8px 16px;\r\n border: none;\r\n border-radius: 8px;\r\n font-size: 13px;\r\n font-weight: 600;\r\n cursor: pointer;\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n transition: all 0.2s;\r\n\r\n &:hover:not(:disabled) {\r\n background: $cDanger;\r\n color: $cWhite;\r\n }\r\n\r\n &:disabled {\r\n opacity: 0.4;\r\n cursor: not-allowed;\r\n }\r\n }\r\n }\r\n }\r\n\r\n .campaigns-list-items {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 0;\r\n padding: 8px 8px;\r\n max-height: 220px;\r\n overflow-y: auto;\r\n\r\n .campaigns-list-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n padding: 6px 12px;\r\n margin: 2px;\r\n border-radius: 6px;\r\n font-size: 13px;\r\n color: $cTextDark;\r\n cursor: pointer;\r\n user-select: none;\r\n transition: background 0.15s;\r\n\r\n &:hover {\r\n background: #F0F4FF;\r\n }\r\n\r\n input[type=\"checkbox\"] {\r\n width: 15px;\r\n height: 15px;\r\n cursor: pointer;\r\n flex-shrink: 0;\r\n }\r\n\r\n .campaigns-list-item-name {\r\n white-space: nowrap;\r\n }\r\n }\r\n }\r\n }\r\n\r\n .campaigns-chart-wrap {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n padding: 20px;\r\n margin-bottom: 20px;\r\n min-height: 350px;\r\n overflow: hidden;\r\n\r\n #container {\r\n max-width: 100%;\r\n }\r\n }\r\n\r\n .campaigns-table-wrap {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n overflow-x: auto;\r\n -ms-overflow-style: none;\r\n scrollbar-width: none;\r\n max-width: 100%;\r\n\r\n &::-webkit-scrollbar {\r\n display: none;\r\n }\r\n\r\n .table {\r\n margin: 0;\r\n width: 100% !important;\r\n\r\n thead th {\r\n background: #F8FAFC;\r\n border-bottom: 2px solid $cBorder;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.5px;\r\n color: #8899A6;\r\n padding: 12px 16px;\r\n white-space: nowrap;\r\n }\r\n\r\n tbody td {\r\n padding: 10px 16px;\r\n vertical-align: middle;\r\n border-bottom: 1px solid #F1F5F9;\r\n font-size: 13px;\r\n }\r\n\r\n tbody tr:hover {\r\n background: #F8FAFC;\r\n }\r\n }\r\n\r\n // DataTables 2.x overrides\r\n .dt-layout-row {\r\n padding: 14px 20px;\r\n margin: 0 !important;\r\n border-top: 1px solid #F1F5F9;\r\n\r\n // Ukryj wiersz z search/length jeśli pusty\r\n &:first-child {\r\n display: none;\r\n }\r\n }\r\n\r\n .dt-info {\r\n font-size: 13px;\r\n color: #8899A6;\r\n }\r\n\r\n .dt-paging {\r\n .pagination {\r\n margin: 0;\r\n padding: 0;\r\n list-style: none;\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n\r\n .page-item {\r\n .page-link {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n min-width: 36px;\r\n width: fit-content;\r\n height: 36px;\r\n padding: 0 14px;\r\n border-radius: 8px;\r\n font-size: 13px;\r\n font-weight: 500;\r\n border: 1px solid $cBorder;\r\n background: $cWhite;\r\n color: $cText;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n text-decoration: none;\r\n line-height: 1;\r\n white-space: nowrap;\r\n\r\n &:hover {\r\n background: #EEF2FF;\r\n color: $cPrimary;\r\n border-color: $cPrimary;\r\n }\r\n }\r\n\r\n &.active .page-link {\r\n background: $cPrimary;\r\n color: $cWhite;\r\n border-color: $cPrimary;\r\n font-weight: 600;\r\n }\r\n\r\n &.disabled .page-link {\r\n opacity: 0.35;\r\n cursor: default;\r\n pointer-events: none;\r\n }\r\n }\r\n }\r\n }\r\n\r\n .dt-processing {\r\n background: rgba($cWhite, 0.9);\r\n color: $cText;\r\n font-size: 14px;\r\n }\r\n }\r\n\r\n .delete-history-entry {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 30px;\r\n height: 30px;\r\n border-radius: 6px;\r\n border: none;\r\n cursor: pointer;\r\n font-size: 12px;\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: $cDanger;\r\n color: $cWhite;\r\n }\r\n }\r\n}\r\n\r\n// ===========================\r\n// PRODUCTS PAGE\r\n// ===========================\r\n.products-page {\r\n .products-header {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 20px;\r\n\r\n h2 {\r\n margin: 0;\r\n font-size: 20px;\r\n font-weight: 600;\r\n color: $cTextDark;\r\n\r\n i {\r\n color: $cPrimary;\r\n margin-right: 8px;\r\n }\r\n }\r\n }\r\n\r\n .products-filters {\r\n display: flex;\r\n flex-wrap: wrap;\r\n align-items: flex-end;\r\n gap: 20px;\r\n margin-bottom: 16px;\r\n\r\n .filter-group {\r\n flex: 1 1 220px;\r\n min-width: 0;\r\n\r\n label {\r\n display: block;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.5px;\r\n color: #8899A6;\r\n margin-bottom: 6px;\r\n\r\n i {\r\n margin-right: 4px;\r\n }\r\n }\r\n\r\n .form-control {\r\n width: 100%;\r\n padding: 10px 14px;\r\n border: 1px solid $cBorder;\r\n border-radius: 8px;\r\n font-size: 14px;\r\n color: $cTextDark;\r\n background: $cWhite;\r\n transition: border-color 0.2s;\r\n\r\n &:focus {\r\n outline: none;\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);\r\n }\r\n }\r\n\r\n select.form-control {\r\n appearance: none;\r\n -webkit-appearance: none;\r\n 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\");\r\n background-repeat: no-repeat;\r\n background-position: right 12px center;\r\n padding-right: 32px;\r\n }\r\n\r\n &.filter-group-client,\r\n &.filter-group-campaign,\r\n &.filter-group-ad-group {\r\n flex: 1 1 260px;\r\n }\r\n\r\n &.filter-group-ad-group {\r\n .ad-group-filter-actions {\r\n display: flex;\r\n gap: 8px;\r\n align-items: center;\r\n\r\n .form-control {\r\n flex: 1 1 auto;\r\n min-width: 0;\r\n }\r\n }\r\n\r\n #delete-products-ad-group {\r\n min-width: 38px;\r\n height: 38px;\r\n border-radius: 6px;\r\n margin: 0;\r\n background: #dc3545;\r\n border: 1px solid #dc3545;\r\n color: #fff;\r\n cursor: pointer;\r\n\r\n &:hover:not(:disabled) {\r\n background: #bb2d3b;\r\n border-color: #bb2d3b;\r\n color: #fff;\r\n }\r\n\r\n &:disabled {\r\n opacity: 0.45;\r\n cursor: default;\r\n background: #dc3545;\r\n border-color: #dc3545;\r\n color: #fff;\r\n }\r\n }\r\n }\r\n\r\n &.filter-group-roas {\r\n flex: 0 0 200px;\r\n }\r\n\r\n &.filter-group-columns {\r\n flex: 0 0 240px;\r\n }\r\n }\r\n }\r\n\r\n .products-scope-alerts {\r\n margin-bottom: 12px;\r\n border: 1px solid #FECACA;\r\n background: #FEF2F2;\r\n border-radius: 8px;\r\n overflow: hidden;\r\n\r\n summary {\r\n cursor: pointer;\r\n list-style: none;\r\n padding: 10px 12px;\r\n font-size: 13px;\r\n font-weight: 600;\r\n color: #991B1B;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n\r\n &::-webkit-details-marker {\r\n display: none;\r\n }\r\n }\r\n\r\n .products-scope-alerts-list {\r\n border-top: 1px solid #FECACA;\r\n background: #FFF;\r\n max-height: 260px;\r\n overflow: auto;\r\n }\r\n\r\n .products-scope-alert-item {\r\n padding: 10px 12px;\r\n border-bottom: 1px solid #F1F5F9;\r\n\r\n &:last-child {\r\n border-bottom: none;\r\n }\r\n }\r\n\r\n .products-scope-alert-meta {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n margin-bottom: 4px;\r\n font-size: 11px;\r\n color: #64748B;\r\n }\r\n\r\n .products-scope-alert-type {\r\n display: inline-flex;\r\n align-items: center;\r\n padding: 2px 6px;\r\n border-radius: 999px;\r\n background: #EEF2FF;\r\n color: #4338CA;\r\n font-weight: 600;\r\n text-transform: uppercase;\r\n letter-spacing: 0.3px;\r\n }\r\n\r\n .products-scope-alert-message {\r\n font-size: 13px;\r\n color: $cTextDark;\r\n line-height: 1.45;\r\n }\r\n }\r\n\r\n .products-actions {\r\n margin-bottom: 12px;\r\n\r\n .btn-danger {\r\n padding: 7px 14px;\r\n font-size: 13px;\r\n border-radius: 6px;\r\n border: none;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n\r\n &:disabled {\r\n opacity: 0.4;\r\n cursor: default;\r\n }\r\n }\r\n }\r\n\r\n .products-table-wrap {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n overflow: hidden;\r\n\r\n .table {\r\n margin: 0;\r\n width: 100% !important;\r\n\r\n thead th {\r\n background: #F8FAFC;\r\n border-bottom: 2px solid $cBorder;\r\n font-size: 11px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: 0.3px;\r\n color: #8899A6;\r\n padding: 10px 8px;\r\n white-space: nowrap;\r\n }\r\n\r\n tbody td {\r\n padding: 6px 8px;\r\n vertical-align: middle;\r\n border-bottom: 1px solid #F1F5F9;\r\n font-size: 12px;\r\n }\r\n\r\n tbody tr:hover {\r\n background: #F8FAFC;\r\n }\r\n\r\n // Kompaktowe inputy w tabeli\r\n input.min_roas,\r\n input.form-control-sm,\r\n select.custom_label_4,\r\n select.form-control-sm {\r\n padding: 3px 6px;\r\n font-size: 12px;\r\n border: 1px solid $cBorder;\r\n border-radius: 4px;\r\n background: $cWhite;\r\n }\r\n }\r\n\r\n // DataTables 2.x overrides (identyczne z campaigns)\r\n .dt-layout-row {\r\n padding: 14px 20px;\r\n margin: 0 !important;\r\n border-top: 1px solid #F1F5F9;\r\n\r\n &:first-child {\r\n display: none;\r\n }\r\n }\r\n\r\n .dt-info {\r\n font-size: 13px;\r\n color: #8899A6;\r\n }\r\n\r\n .dt-paging {\r\n .pagination {\r\n margin: 0;\r\n padding: 0;\r\n list-style: none;\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n\r\n .page-item {\r\n .page-link {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n min-width: 36px;\r\n height: 36px;\r\n padding: 0 14px;\r\n border-radius: 8px;\r\n font-size: 13px;\r\n font-weight: 500;\r\n border: 1px solid $cBorder;\r\n background: $cWhite;\r\n color: $cText;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n text-decoration: none;\r\n line-height: 1;\r\n white-space: nowrap;\r\n\r\n &:hover {\r\n background: #EEF2FF;\r\n color: $cPrimary;\r\n border-color: $cPrimary;\r\n }\r\n }\r\n\r\n &.active .page-link {\r\n background: $cPrimary;\r\n color: $cWhite;\r\n border-color: $cPrimary;\r\n font-weight: 600;\r\n }\r\n\r\n &.disabled .page-link {\r\n opacity: 0.35;\r\n cursor: default;\r\n pointer-events: none;\r\n }\r\n }\r\n }\r\n }\r\n\r\n .dt-processing {\r\n background: rgba($cWhite, 0.9);\r\n color: $cText;\r\n font-size: 14px;\r\n }\r\n }\r\n\r\n // Przycisk usuwania w wierszu\r\n .delete-product {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 28px;\r\n height: 28px;\r\n border-radius: 6px;\r\n border: none;\r\n cursor: pointer;\r\n font-size: 12px;\r\n background: #FFF5F5;\r\n color: $cDanger;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: $cDanger;\r\n color: $cWhite;\r\n }\r\n }\r\n\r\n // Przycisk edycji w wierszu\r\n .edit-product-title {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 28px;\r\n height: 28px;\r\n border-radius: 6px;\r\n border: none;\r\n cursor: pointer;\r\n font-size: 12px;\r\n background: #EEF2FF;\r\n color: $cPrimary;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: $cPrimary;\r\n color: $cWhite;\r\n }\r\n }\r\n}\r\n\r\n// --- Popup edycji produktu: AI suggest ---\r\n.desc-header {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 4px;\r\n\r\n label {\r\n margin: 0;\r\n }\r\n}\r\n\r\n.desc-tabs {\r\n display: flex;\r\n gap: 2px;\r\n background: #eee;\r\n border-radius: 6px;\r\n padding: 2px;\r\n}\r\n\r\n.desc-tab {\r\n border: none;\r\n background: transparent;\r\n padding: 4px 12px;\r\n font-size: 12px;\r\n border-radius: 4px;\r\n cursor: pointer;\r\n color: #666;\r\n transition: all .15s ease;\r\n\r\n i {\r\n margin-right: 4px;\r\n }\r\n\r\n &.active {\r\n background: #fff;\r\n color: #333;\r\n box-shadow: 0 1px 3px rgba(0, 0, 0, .12);\r\n font-weight: 500;\r\n }\r\n\r\n &:hover:not(.active) {\r\n color: #333;\r\n }\r\n}\r\n\r\n.desc-wrap {\r\n flex: 1;\r\n min-width: 0;\r\n}\r\n\r\n.desc-preview {\r\n\r\n ul,\r\n ol {\r\n margin: 6px 0;\r\n padding-left: 20px;\r\n }\r\n\r\n li {\r\n margin-bottom: 3px;\r\n }\r\n\r\n b,\r\n strong {\r\n font-weight: 600;\r\n }\r\n}\r\n\r\n.input-with-ai {\r\n display: flex;\r\n gap: 8px;\r\n align-items: flex-start;\r\n\r\n .form-control {\r\n flex: 1;\r\n }\r\n}\r\n\r\n.btn-ai-suggest {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 4px;\r\n padding: 6px 12px;\r\n border-radius: 8px;\r\n border: 1px solid #C084FC;\r\n background: linear-gradient(135deg, #F3E8FF, #EDE9FE);\r\n color: #7C3AED;\r\n font-size: 12px;\r\n font-weight: 600;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n white-space: nowrap;\r\n min-height: 38px;\r\n\r\n i {\r\n font-size: 13px;\r\n }\r\n\r\n &:hover {\r\n background: linear-gradient(135deg, #7C3AED, #6D28D9);\r\n color: #FFF;\r\n border-color: #6D28D9;\r\n }\r\n\r\n &:disabled {\r\n opacity: 0.7;\r\n cursor: wait;\r\n }\r\n\r\n &.btn-ai-claude {\r\n border-color: #D97706;\r\n background: linear-gradient(135deg, #FEF3C7, #FDE68A);\r\n color: #92400E;\r\n\r\n &:hover {\r\n background: linear-gradient(135deg, #D97706, #B45309);\r\n color: #FFF;\r\n border-color: #B45309;\r\n }\r\n }\r\n}\r\n\r\n// --- Form container ---\r\n.form_container {\r\n background: $cWhite;\r\n padding: 25px;\r\n max-width: 1300px;\r\n border-radius: 8px;\r\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);\r\n\r\n &.full {\r\n max-width: 100%;\r\n }\r\n\r\n .form_group {\r\n margin-bottom: 12px;\r\n display: flex;\r\n\r\n >.label {\r\n width: 300px;\r\n display: inline-flex;\r\n align-items: flex-start;\r\n justify-content: right;\r\n padding-right: 12px;\r\n }\r\n\r\n .input {\r\n width: calc(100% - 300px);\r\n }\r\n }\r\n}\r\n\r\n// --- Default popup ---\r\n.default_popup {\r\n position: fixed;\r\n top: 0;\r\n left: 0;\r\n width: 100%;\r\n height: 100%;\r\n background: rgba(0, 0, 0, 0.45);\r\n display: none;\r\n z-index: 2000;\r\n\r\n .popup_content {\r\n position: absolute;\r\n top: 50%;\r\n left: 50%;\r\n transform: translate(-50%, -50%);\r\n background: $cWhite;\r\n padding: 25px;\r\n border-radius: 10px;\r\n max-width: 1140px;\r\n width: 95%;\r\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);\r\n\r\n .popup_header {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n margin-bottom: 15px;\r\n\r\n .title {\r\n font-size: 18px;\r\n font-weight: 600;\r\n }\r\n }\r\n\r\n .close {\r\n cursor: pointer;\r\n color: #A0AEC0;\r\n font-size: 18px;\r\n padding: 4px;\r\n\r\n &:hover {\r\n color: $cDanger;\r\n }\r\n }\r\n }\r\n}\r\n\r\n// --- DataTables override ---\r\n.dt-layout-table {\r\n margin-bottom: 20px;\r\n}\r\n\r\n.pagination {\r\n button {\r\n border: 1px solid $cBorder;\r\n background: $cWhite;\r\n display: inline-flex;\r\n height: 32px;\r\n width: 32px;\r\n align-items: center;\r\n justify-content: center;\r\n margin: 0 2px;\r\n border-radius: 4px;\r\n transition: all 0.2s;\r\n cursor: pointer;\r\n\r\n &:hover {\r\n background: $cContentBg;\r\n border-color: $cPrimary;\r\n }\r\n }\r\n}\r\n\r\n// ===========================\r\n// PRODUCTS specific\r\n// ===========================\r\ntable#products {\r\n a {\r\n color: inherit;\r\n text-decoration: none;\r\n }\r\n\r\n .table-product-title {\r\n display: flex;\r\n justify-content: space-between;\r\n }\r\n\r\n .edit-product-title {\r\n display: flex;\r\n height: 25px;\r\n align-items: center;\r\n justify-content: center;\r\n width: 25px;\r\n cursor: pointer;\r\n background: $cWhite;\r\n border: 1px solid #CBD5E0;\r\n color: #CBD5E0;\r\n border-radius: 4px;\r\n\r\n &:hover {\r\n background: #CBD5E0;\r\n color: $cWhite;\r\n }\r\n }\r\n\r\n a.custom_name {\r\n color: $cGreenLight !important;\r\n }\r\n}\r\n\r\n// --- Product history ---\r\n.product-history-page {\r\n .product-history-meta {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 8px;\r\n margin-bottom: 14px;\r\n\r\n span {\r\n display: inline-flex;\r\n align-items: center;\r\n padding: 5px 10px;\r\n border-radius: 999px;\r\n font-size: 12px;\r\n font-weight: 600;\r\n color: $cText;\r\n background: #EEF2FF;\r\n border: 1px solid #D9E2FF;\r\n }\r\n }\r\n\r\n .product-history-chart-wrap {\r\n background: $cWhite;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n padding: 20px;\r\n margin-bottom: 16px;\r\n }\r\n\r\n .chart-with-form {\r\n display: flex;\r\n gap: 20px;\r\n align-items: flex-start;\r\n }\r\n\r\n .chart-area {\r\n flex: 1 1 auto;\r\n min-width: 0;\r\n }\r\n\r\n .product-history-chart {\r\n min-height: 360px;\r\n }\r\n\r\n .comment-form {\r\n width: 340px;\r\n flex: 0 0 340px;\r\n background: #F8FAFC;\r\n border: 1px solid $cBorder;\r\n border-radius: 10px;\r\n padding: 14px;\r\n\r\n .form-group {\r\n margin-bottom: 12px;\r\n }\r\n\r\n label {\r\n display: block;\r\n font-weight: 600;\r\n margin-bottom: 6px;\r\n font-size: 13px;\r\n color: #52606D;\r\n }\r\n\r\n input[type=\"date\"],\r\n textarea {\r\n width: 100%;\r\n border: 1px solid $cBorder;\r\n border-radius: 6px;\r\n padding: 8px 12px;\r\n font-size: 14px;\r\n font-family: \"Roboto\", sans-serif;\r\n background: $cWhite;\r\n\r\n &:focus {\r\n outline: none;\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);\r\n }\r\n }\r\n\r\n textarea {\r\n min-height: 110px;\r\n resize: vertical;\r\n }\r\n\r\n .btn {\r\n width: 100%;\r\n justify-content: center;\r\n padding: 10px 16px;\r\n }\r\n\r\n .btn[disabled] {\r\n opacity: 0.6;\r\n cursor: not-allowed;\r\n }\r\n }\r\n\r\n .products-table-wrap {\r\n overflow-x: auto;\r\n\r\n .table {\r\n min-width: 980px;\r\n }\r\n\r\n .comment-cell {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n gap: 10px;\r\n }\r\n\r\n .comment-text {\r\n word-break: break-word;\r\n }\r\n\r\n .delete-comment {\r\n color: $cDanger;\r\n text-decoration: none;\r\n font-weight: 600;\r\n white-space: nowrap;\r\n\r\n &:hover {\r\n text-decoration: underline;\r\n }\r\n }\r\n\r\n .dt-paging .pagination .page-item {\r\n list-style: none;\r\n }\r\n }\r\n}\r\n\r\n.cron-status-overview {\r\n display: flex;\r\n flex-wrap: wrap;\r\n gap: 10px 20px;\r\n margin-bottom: 20px;\r\n color: $cText;\r\n font-size: 13px;\r\n}\r\n\r\n.cron-progress-list {\r\n margin-bottom: 20px;\r\n}\r\n\r\n.cron-schedule-list {\r\n margin-bottom: 20px;\r\n}\r\n\r\n.cron-schedule-item {\r\n border: 1px solid #DFE7F0;\r\n background: #F4F8FD;\r\n border-radius: 8px;\r\n padding: 9px 12px;\r\n margin-bottom: 8px;\r\n\r\n &:last-child {\r\n margin-bottom: 0;\r\n }\r\n\r\n strong {\r\n display: block;\r\n color: $cTextDark;\r\n font-size: 13px;\r\n font-weight: 700;\r\n margin-bottom: 2px;\r\n }\r\n\r\n small {\r\n display: block;\r\n color: #667788;\r\n font-size: 12px;\r\n line-height: 1.35;\r\n }\r\n}\r\n\r\n.cron-progress-item {\r\n margin-bottom: 14px;\r\n\r\n &:last-child {\r\n margin-bottom: 0;\r\n }\r\n\r\n .cron-progress-head {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n gap: 12px;\r\n margin-bottom: 6px;\r\n font-size: 13px;\r\n\r\n strong {\r\n color: $cTextDark;\r\n font-weight: 600;\r\n }\r\n\r\n span {\r\n color: #6B7A89;\r\n font-size: 12px;\r\n font-weight: 600;\r\n white-space: nowrap;\r\n }\r\n }\r\n\r\n small {\r\n display: block;\r\n margin-top: 5px;\r\n color: #778899;\r\n font-size: 12px;\r\n }\r\n}\r\n\r\n.cron-progress-bar {\r\n width: 100%;\r\n height: 10px;\r\n border-radius: 999px;\r\n background: #E9EEF5;\r\n overflow: hidden;\r\n\r\n >span {\r\n display: block;\r\n height: 100%;\r\n background: linear-gradient(90deg, #5A9BFF 0%, #2E6BDF 100%);\r\n }\r\n}\r\n\r\n.cron-url-list {\r\n margin-bottom: 20px;\r\n}\r\n\r\n.cron-url-item {\r\n border: 1px solid $cBorder;\r\n border-radius: 8px;\r\n background: #F8FAFC;\r\n padding: 10px 12px;\r\n margin-bottom: 10px;\r\n\r\n &:last-child {\r\n margin-bottom: 0;\r\n }\r\n\r\n .cron-url-top {\r\n display: flex;\r\n justify-content: space-between;\r\n align-items: center;\r\n gap: 8px;\r\n margin-bottom: 6px;\r\n\r\n strong {\r\n color: $cTextDark;\r\n font-size: 13px;\r\n font-weight: 600;\r\n }\r\n\r\n small {\r\n color: #7A8794;\r\n font-size: 11px;\r\n white-space: nowrap;\r\n }\r\n }\r\n\r\n code {\r\n display: block;\r\n background: #EEF2F7;\r\n border: 1px solid #DDE4ED;\r\n border-radius: 6px;\r\n padding: 6px 8px;\r\n color: #2E3B49;\r\n font-size: 12px;\r\n overflow-x: auto;\r\n }\r\n\r\n .cron-url-plan {\r\n display: block;\r\n color: #6C7B8A;\r\n font-size: 11px;\r\n margin-bottom: 6px;\r\n }\r\n}\r\n\r\n@media (max-width: 1200px) {\r\n .product-history-page {\r\n .chart-with-form {\r\n flex-direction: column;\r\n }\r\n\r\n .comment-form {\r\n width: 100%;\r\n flex: 1 1 auto;\r\n }\r\n }\r\n}\r\n\r\n// --- Select2 w modalu ---\r\n.jconfirm-box .form-group .select2-container,\r\n.adspro-dialog-box .form-group .select2-container {\r\n width: 100% !important;\r\n margin-top: 8px;\r\n}\r\n\r\n.jconfirm-box .select2-container--default .select2-selection--single,\r\n.adspro-dialog-box .select2-container--default .select2-selection--single {\r\n background-color: $cWhite;\r\n border: 1px solid $cBorder;\r\n border-radius: 6px;\r\n min-height: 42px;\r\n display: flex;\r\n align-items: center;\r\n padding: 4px 12px;\r\n box-shadow: none;\r\n transition: border-color 0.2s, box-shadow 0.2s;\r\n font-size: 14px;\r\n}\r\n\r\n.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__rendered,\r\n.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__rendered {\r\n padding-left: 0;\r\n line-height: 1.4;\r\n color: #495057;\r\n}\r\n\r\n.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__placeholder,\r\n.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__placeholder {\r\n color: #CBD5E0;\r\n}\r\n\r\n.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__arrow,\r\n.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__arrow {\r\n height: 100%;\r\n right: 8px;\r\n}\r\n\r\n.jconfirm-box .select2-container--default.select2-container--focus .select2-selection--single,\r\n.jconfirm-box .select2-container--default .select2-selection--single:hover,\r\n.adspro-dialog-box .select2-container--default.select2-container--focus .select2-selection--single,\r\n.adspro-dialog-box .select2-container--default .select2-selection--single:hover {\r\n border-color: $cPrimary;\r\n box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);\r\n outline: 0;\r\n}\r\n\r\n.jconfirm-box .select2-container .select2-dropdown,\r\n.adspro-dialog-box .select2-container .select2-dropdown {\r\n border-color: $cBorder;\r\n border-radius: 0 0 6px 6px;\r\n font-size: 14px;\r\n}\r\n\r\n.jconfirm-box .select2-container .select2-search--dropdown .select2-search__field,\r\n.adspro-dialog-box .select2-container .select2-search--dropdown .select2-search__field {\r\n padding: 6px 10px;\r\n border-radius: 4px;\r\n border: 1px solid $cBorder;\r\n font-size: 14px;\r\n}\r\n\r\n.jconfirm-box .select2-container--default .select2-results__option--highlighted[aria-selected],\r\n.adspro-dialog-box .select2-container--default .select2-results__option--highlighted[aria-selected] {\r\n background-color: $cPrimary;\r\n color: $cWhite;\r\n}\r\n\r\n// ===========================\r\n// RESPONSYWNOŚĆ\r\n// ===========================\r\n@media (max-width: 992px) {\r\n .sidebar {\r\n transform: translateX(-100%);\r\n\r\n &.mobile-open {\r\n transform: translateX(0);\r\n }\r\n }\r\n\r\n .main-wrapper {\r\n margin-left: 0 !important;\r\n }\r\n}\r\n\r\n// ===========================\r\n// CAMPAIGN TERMS VIEW\r\n// ===========================\r\n.campaign-terms-wrap {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 20px;\r\n margin-top: 20px;\r\n}\r\n\r\n.campaign-terms-page {\r\n max-width: 100%;\r\n overflow: hidden;\r\n\r\n .campaigns-filters {\r\n flex-wrap: wrap;\r\n\r\n .filter-group {\r\n min-width: 220px;\r\n\r\n &.terms-columns-group {\r\n min-width: 280px;\r\n }\r\n }\r\n }\r\n\r\n .terms-card-toggle {\r\n margin-left: auto;\r\n width: 28px;\r\n height: 28px;\r\n border: 1px solid #E2E8F0;\r\n border-radius: 6px;\r\n background: #FFFFFF;\r\n color: #475569;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: #F8FAFC;\r\n border-color: #CBD5E1;\r\n }\r\n }\r\n\r\n .terms-adgroups-card.is-collapsed .campaigns-extra-table-wrap {\r\n display: none;\r\n }\r\n\r\n .terms-search-toolbar {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n padding: 10px 12px;\r\n border-bottom: 1px solid #EEF2F7;\r\n background: #FFFFFF;\r\n\r\n label {\r\n font-size: 12px;\r\n font-weight: 600;\r\n color: #475569;\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n margin: 0;\r\n white-space: nowrap;\r\n }\r\n\r\n .terms-search-toolbar-label {\r\n min-width: 86px;\r\n }\r\n\r\n #terms_min_clicks_all,\r\n #terms_max_clicks_all {\r\n width: 160px;\r\n height: 32px;\r\n }\r\n\r\n #terms_min_conversions_all,\r\n #terms_max_conversions_all {\r\n width: 130px;\r\n max-width: 130px;\r\n }\r\n }\r\n\r\n .terms-search-selected-label {\r\n margin: 0;\r\n font-size: 12px;\r\n color: #475569;\r\n font-weight: 600;\r\n white-space: nowrap;\r\n }\r\n\r\n .terms-ai-analyze-btn {\r\n margin-left: auto;\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n height: 32px;\r\n padding: 0 12px;\r\n border-radius: 6px;\r\n border: 1px solid #BFDBFE;\r\n background: #EFF6FF;\r\n color: #1D4ED8;\r\n font-size: 12px;\r\n font-weight: 600;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: #DBEAFE;\r\n border-color: #93C5FD;\r\n }\r\n\r\n &:disabled {\r\n opacity: 0.6;\r\n cursor: wait;\r\n }\r\n }\r\n\r\n .terms-negative-toolbar {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n padding: 10px 12px;\r\n border-bottom: 1px solid #EEF2F7;\r\n background: #FFFFFF;\r\n }\r\n\r\n .terms-negative-bulk-btn {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 6px;\r\n height: 32px;\r\n padding: 0 12px;\r\n border-radius: 6px;\r\n border: 1px solid #FECACA;\r\n background: #FEF2F2;\r\n color: #DC2626;\r\n font-size: 12px;\r\n font-weight: 600;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n\r\n &:hover {\r\n background: #FEE2E2;\r\n border-color: #FCA5A5;\r\n }\r\n\r\n &:disabled {\r\n opacity: 0.5;\r\n cursor: not-allowed;\r\n }\r\n }\r\n\r\n table.campaigns-extra-table>thead>tr>th {\r\n position: sticky;\r\n top: 0;\r\n z-index: 2;\r\n background-color: #111827 !important;\r\n color: #E5E7EB !important;\r\n border-bottom: 1px solid #0B1220 !important;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: .4px;\r\n padding: 10px 12px;\r\n white-space: nowrap;\r\n }\r\n\r\n #terms_search_table thead th .dt-column-order,\r\n #terms_negative_table thead th .dt-column-order {\r\n display: none !important;\r\n }\r\n\r\n #terms_search_table thead th.dt-orderable-asc,\r\n #terms_search_table thead th.dt-orderable-desc,\r\n #terms_negative_table thead th.dt-orderable-asc,\r\n #terms_negative_table thead th.dt-orderable-desc {\r\n cursor: pointer;\r\n padding-right: 34px;\r\n overflow: hidden;\r\n }\r\n\r\n #terms_search_table thead th .dt-column-title,\r\n #terms_negative_table thead th .dt-column-title {\r\n display: block;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n padding-right: 2px;\r\n }\r\n\r\n #terms_search_table thead th.dt-orderable-asc::after,\r\n #terms_search_table thead th.dt-orderable-desc::after,\r\n #terms_negative_table thead th.dt-orderable-asc::after,\r\n #terms_negative_table thead th.dt-orderable-desc::after {\r\n content: '\\2195';\r\n position: absolute;\r\n right: 10px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n width: 16px;\r\n height: 16px;\r\n border-radius: 999px;\r\n font-size: 12px;\r\n font-weight: 700;\r\n line-height: 16px;\r\n text-align: center;\r\n color: #E5E7EB;\r\n background: #374151;\r\n }\r\n\r\n #terms_search_table thead th.dt-ordering-asc::after,\r\n #terms_negative_table thead th.dt-ordering-asc::after,\r\n #terms_search_table thead th[aria-sort=\"ascending\"]::after,\r\n #terms_negative_table thead th[aria-sort=\"ascending\"]::after {\r\n content: '\\25B2';\r\n color: #FFFFFF;\r\n background: #2563EB;\r\n }\r\n\r\n #terms_search_table thead th.dt-ordering-desc::after,\r\n #terms_negative_table thead th.dt-ordering-desc::after,\r\n #terms_search_table thead th[aria-sort=\"descending\"]::after,\r\n #terms_negative_table thead th[aria-sort=\"descending\"]::after {\r\n content: '\\25BC';\r\n color: #FFFFFF;\r\n background: #2563EB;\r\n }\r\n\r\n #terms_negative_select_all,\r\n .terms-negative-select-row,\r\n #terms_search_select_all,\r\n .terms-search-select-row {\r\n width: 14px;\r\n height: 14px;\r\n cursor: pointer;\r\n }\r\n\r\n .dt-layout-row:first-child {\r\n display: none;\r\n }\r\n\r\n .dt-layout-row {\r\n padding: 10px 12px;\r\n margin: 0 !important;\r\n border-top: 1px solid #F1F5F9;\r\n }\r\n\r\n .dt-info {\r\n font-size: 12px;\r\n color: #64748B;\r\n }\r\n\r\n .dt-paging .pagination {\r\n margin: 0;\r\n padding: 0;\r\n list-style: none !important;\r\n display: flex;\r\n align-items: center;\r\n gap: 6px;\r\n\r\n .page-item {\r\n list-style: none !important;\r\n\r\n .page-link {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n min-width: 36px;\r\n width: fit-content;\r\n height: 32px;\r\n padding: 0 12px;\r\n border-radius: 6px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n border: 1px solid #E2E8F0;\r\n background: #FFFFFF;\r\n color: #4E5E6A;\r\n text-decoration: none;\r\n line-height: 1;\r\n white-space: nowrap;\r\n\r\n &:hover {\r\n background: #EEF2FF;\r\n color: #6690F4;\r\n border-color: #6690F4;\r\n }\r\n }\r\n\r\n &.previous .page-link,\r\n &.next .page-link {\r\n min-width: 72px;\r\n }\r\n\r\n &.active .page-link {\r\n background: #6690F4;\r\n color: #FFFFFF;\r\n border-color: #6690F4;\r\n }\r\n\r\n &.disabled .page-link {\r\n opacity: 0.35;\r\n cursor: default;\r\n pointer-events: none;\r\n }\r\n }\r\n }\r\n}\r\n\r\n.terms-columns-box {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 6px;\r\n}\r\n\r\n.terms-columns-control {\r\n border: 1px solid #E2E8F0;\r\n border-radius: 6px;\r\n background: #FFFFFF;\r\n overflow: hidden;\r\n\r\n summary {\r\n cursor: pointer;\r\n padding: 8px 10px;\r\n font-size: 12px;\r\n font-weight: 600;\r\n color: #334155;\r\n list-style: none;\r\n\r\n &::-webkit-details-marker {\r\n display: none;\r\n }\r\n\r\n &::after {\r\n content: '\\25BC';\r\n float: right;\r\n font-size: 10px;\r\n color: #64748B;\r\n margin-top: 2px;\r\n }\r\n }\r\n\r\n &[open] summary::after {\r\n content: '\\25B2';\r\n }\r\n}\r\n\r\n.terms-columns-list {\r\n border-top: 1px solid #EEF2F7;\r\n padding: 8px 10px;\r\n max-height: 180px;\r\n overflow-y: auto;\r\n\r\n .terms-col-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n font-size: 12px;\r\n color: #334155;\r\n margin-bottom: 6px;\r\n\r\n &:last-child {\r\n margin-bottom: 0;\r\n }\r\n\r\n input[type=checkbox] {\r\n margin: 0;\r\n }\r\n }\r\n}\r\n\r\n.campaigns-extra-card {\r\n background: #FFFFFF;\r\n border-radius: 10px;\r\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);\r\n overflow: hidden;\r\n}\r\n\r\n.campaigns-extra-card-title {\r\n padding: 14px 16px;\r\n border-bottom: 1px solid #E2E8F0;\r\n font-size: 13px;\r\n font-weight: 700;\r\n color: #334155;\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n\r\n .terms-card-title-label {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 8px;\r\n }\r\n}\r\n\r\n.campaigns-extra-table-wrap {\r\n overflow: auto;\r\n}\r\n\r\n.campaigns-extra-table {\r\n margin: 0;\r\n width: 100%;\r\n table-layout: fixed;\r\n\r\n tbody td {\r\n padding: 9px 12px;\r\n border-bottom: 1px solid #F1F5F9;\r\n font-size: 13px;\r\n color: #334155;\r\n vertical-align: middle;\r\n white-space: nowrap;\r\n }\r\n\r\n td.num-cell {\r\n text-align: right;\r\n white-space: nowrap;\r\n }\r\n\r\n td.text-cell {\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n }\r\n\r\n th.terms-negative-select-cell,\r\n td.terms-negative-select-cell,\r\n th.terms-search-select-cell,\r\n td.terms-search-select-cell {\r\n text-align: center;\r\n }\r\n\r\n th.phrase-nowrap,\r\n td.phrase-nowrap {\r\n white-space: nowrap !important;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n }\r\n\r\n .terms-add-negative-btn,\r\n .terms-remove-negative-btn {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n width: 28px;\r\n height: 28px;\r\n border-radius: 6px;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n }\r\n\r\n .terms-add-negative-btn {\r\n border: 1px solid #E2E8F0;\r\n background: #EEF2FF;\r\n color: #3B82F6;\r\n\r\n &:hover {\r\n background: #3B82F6;\r\n color: #FFFFFF;\r\n border-color: #3B82F6;\r\n }\r\n }\r\n\r\n .terms-remove-negative-btn {\r\n border: 1px solid #FECACA;\r\n background: #FEF2F2;\r\n color: #DC2626;\r\n\r\n &:hover {\r\n background: #DC2626;\r\n color: #FFFFFF;\r\n border-color: #DC2626;\r\n }\r\n }\r\n\r\n tbody tr:hover {\r\n background: #F8FAFC;\r\n }\r\n\r\n tbody tr.term-is-negative td {\r\n color: #DC2626;\r\n }\r\n\r\n tbody tr.term-is-negative:hover {\r\n background: #FEF2F2;\r\n }\r\n}\r\n\r\n.campaigns-empty-row {\r\n text-align: center;\r\n color: #94A3B8 !important;\r\n font-style: italic;\r\n}\r\n\r\n.terms-ai-modal-toolbar {\r\n display: flex;\r\n align-items: center;\r\n gap: 10px;\r\n margin-bottom: 10px;\r\n\r\n label {\r\n font-size: 12px;\r\n font-weight: 600;\r\n color: #334155;\r\n margin: 0;\r\n }\r\n\r\n .form-control {\r\n width: 200px;\r\n height: 32px;\r\n }\r\n}\r\n\r\n.terms-ai-summary {\r\n font-size: 12px;\r\n color: #64748B;\r\n margin-bottom: 10px;\r\n}\r\n\r\n.terms-ai-results-wrap {\r\n border: 1px solid #E2E8F0;\r\n border-radius: 8px;\r\n max-height: 420px;\r\n overflow: auto;\r\n}\r\n\r\n.terms-ai-results-table {\r\n width: 100%;\r\n border-collapse: collapse;\r\n font-size: 12px;\r\n\r\n th,\r\n td {\r\n border-bottom: 1px solid #EEF2F7;\r\n padding: 8px;\r\n vertical-align: middle;\r\n }\r\n\r\n th {\r\n position: sticky;\r\n top: 0;\r\n background: #F8FAFC;\r\n color: #334155;\r\n font-weight: 700;\r\n }\r\n\r\n td.term-col {\r\n min-width: 260px;\r\n max-width: 380px;\r\n white-space: nowrap;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n }\r\n\r\n td.reason-col {\r\n min-width: 320px;\r\n }\r\n}\r\n\r\n.terms-ai-action-badge {\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border-radius: 999px;\r\n padding: 2px 8px;\r\n font-size: 11px;\r\n font-weight: 700;\r\n\r\n &.action-exclude {\r\n background: #FEE2E2;\r\n color: #B91C1C;\r\n }\r\n\r\n &.action-keep {\r\n background: #DCFCE7;\r\n color: #166534;\r\n }\r\n}\r\n\r\n// ===========================\r\n// PRODUCTS VIEW (INLINE MOVED)\r\n// ===========================\r\n.products-page .products-filters .filter-group.filter-group-columns {\r\n min-width: 240px;\r\n}\r\n\r\n.products-columns-control {\r\n border: 1px solid #E2E8F0;\r\n border-radius: 6px;\r\n background: #FFFFFF;\r\n overflow: hidden;\r\n\r\n summary {\r\n cursor: pointer;\r\n padding: 8px 10px;\r\n font-size: 12px;\r\n font-weight: 600;\r\n color: #334155;\r\n list-style: none;\r\n\r\n &::-webkit-details-marker {\r\n display: none;\r\n }\r\n\r\n &::after {\r\n content: '\\25BC';\r\n float: right;\r\n font-size: 10px;\r\n color: #64748B;\r\n margin-top: 2px;\r\n }\r\n }\r\n\r\n &[open] summary::after {\r\n content: '\\25B2';\r\n }\r\n}\r\n\r\n.products-columns-list {\r\n border-top: 1px solid #EEF2F7;\r\n padding: 8px 10px;\r\n max-height: 220px;\r\n overflow-y: auto;\r\n\r\n .products-col-item {\r\n display: flex;\r\n align-items: center;\r\n gap: 8px;\r\n font-size: 12px;\r\n color: #334155;\r\n margin-bottom: 6px;\r\n\r\n &:last-child {\r\n margin-bottom: 0;\r\n }\r\n\r\n input[type=checkbox] {\r\n margin: 0;\r\n }\r\n }\r\n}\r\n\r\n#products {\r\n\r\n th:last-child,\r\n td:last-child {\r\n white-space: nowrap;\r\n }\r\n\r\n .products-row-actions {\r\n display: inline-flex;\r\n align-items: center;\r\n gap: 4px;\r\n\r\n .btn {\r\n width: 38px;\r\n height: 32px;\r\n padding: 0;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n border-radius: 4px !important;\r\n\r\n i {\r\n line-height: 1;\r\n }\r\n }\r\n }\r\n}\r\n\r\n.products-page table#products>thead>tr>th {\r\n position: sticky;\r\n top: 0;\r\n z-index: 2;\r\n background-color: #111827 !important;\r\n color: #E5E7EB !important;\r\n border-bottom: 1px solid #0B1220 !important;\r\n font-size: 12px;\r\n font-weight: 700;\r\n text-transform: uppercase;\r\n letter-spacing: .4px;\r\n padding: 10px 12px;\r\n white-space: nowrap;\r\n}\r\n\r\n.products-page #products thead th .dt-column-order {\r\n display: none !important;\r\n}\r\n\r\n.products-page #products thead th.dt-orderable-asc,\r\n.products-page #products thead th.dt-orderable-desc {\r\n cursor: pointer;\r\n padding-right: 34px;\r\n overflow: hidden;\r\n}\r\n\r\n.products-page #products thead th .dt-column-title {\r\n display: block;\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n padding-right: 2px;\r\n}\r\n\r\n.products-page #products thead th.dt-orderable-asc::after,\r\n.products-page #products thead th.dt-orderable-desc::after {\r\n content: '\\2195';\r\n position: absolute;\r\n right: 10px;\r\n top: 50%;\r\n transform: translateY(-50%);\r\n width: 16px;\r\n height: 16px;\r\n border-radius: 999px;\r\n font-size: 12px;\r\n font-weight: 700;\r\n line-height: 16px;\r\n text-align: center;\r\n color: #E5E7EB;\r\n background: #374151;\r\n}\r\n\r\n.products-page #products thead th.dt-ordering-asc::after,\r\n.products-page #products thead th[aria-sort=\"ascending\"]::after {\r\n content: '\\25B2';\r\n color: #FFFFFF;\r\n background: #2563EB;\r\n}\r\n\r\n.products-page #products thead th.dt-ordering-desc::after,\r\n.products-page #products thead th[aria-sort=\"descending\"]::after {\r\n content: '\\25BC';\r\n color: #FFFFFF;\r\n background: #2563EB;\r\n}"]} \ No newline at end of file diff --git a/layout/style.scss b/layout/style.scss index 76ec925..b53d150 100644 --- a/layout/style.scss +++ b/layout/style.scss @@ -1856,6 +1856,44 @@ table { flex: 1 1 260px; } + &.filter-group-ad-group { + .ad-group-filter-actions { + display: flex; + gap: 8px; + align-items: center; + + .form-control { + flex: 1 1 auto; + min-width: 0; + } + } + + #delete-products-ad-group { + min-width: 38px; + height: 38px; + border-radius: 6px; + margin: 0; + background: #dc3545; + border: 1px solid #dc3545; + color: #fff; + cursor: pointer; + + &:hover:not(:disabled) { + background: #bb2d3b; + border-color: #bb2d3b; + color: #fff; + } + + &:disabled { + opacity: 0.45; + cursor: default; + background: #dc3545; + border-color: #dc3545; + color: #fff; + } + } + } + &.filter-group-roas { flex: 0 0 200px; } @@ -1866,6 +1904,73 @@ table { } } + .products-scope-alerts { + margin-bottom: 12px; + border: 1px solid #FECACA; + background: #FEF2F2; + border-radius: 8px; + overflow: hidden; + + summary { + cursor: pointer; + list-style: none; + padding: 10px 12px; + font-size: 13px; + font-weight: 600; + color: #991B1B; + display: flex; + align-items: center; + gap: 8px; + + &::-webkit-details-marker { + display: none; + } + } + + .products-scope-alerts-list { + border-top: 1px solid #FECACA; + background: #FFF; + max-height: 260px; + overflow: auto; + } + + .products-scope-alert-item { + padding: 10px 12px; + border-bottom: 1px solid #F1F5F9; + + &:last-child { + border-bottom: none; + } + } + + .products-scope-alert-meta { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + font-size: 11px; + color: #64748B; + } + + .products-scope-alert-type { + display: inline-flex; + align-items: center; + padding: 2px 6px; + border-radius: 999px; + background: #EEF2FF; + color: #4338CA; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.3px; + } + + .products-scope-alert-message { + font-size: 13px; + color: $cTextDark; + line-height: 1.45; + } + } + .products-actions { margin-bottom: 12px; @@ -2458,6 +2563,37 @@ table#products { margin-bottom: 20px; } +.cron-schedule-list { + margin-bottom: 20px; +} + +.cron-schedule-item { + border: 1px solid #DFE7F0; + background: #F4F8FD; + border-radius: 8px; + padding: 9px 12px; + margin-bottom: 8px; + + &:last-child { + margin-bottom: 0; + } + + strong { + display: block; + color: $cTextDark; + font-size: 13px; + font-weight: 700; + margin-bottom: 2px; + } + + small { + display: block; + color: #667788; + font-size: 12px; + line-height: 1.35; + } +} + .cron-progress-item { margin-bottom: 14px; @@ -2553,6 +2689,13 @@ table#products { font-size: 12px; overflow-x: auto; } + + .cron-url-plan { + display: block; + color: #6C7B8A; + font-size: 11px; + margin-bottom: 6px; + } } @media (max-width: 1200px) { diff --git a/migrations/016_products_model_unification.sql b/migrations/016_products_model_unification.sql new file mode 100644 index 0000000..d3a936d --- /dev/null +++ b/migrations/016_products_model_unification.sql @@ -0,0 +1,165 @@ +-- Migracja: unifikacja modelu danych produktow +-- Cel: +-- 1) przeniesienie danych produktowych do tabeli products (kolumny dotad trzymane glownie w products_data) +-- 2) dodanie tabeli agregatow products_aggregate (30 dni + all-time) + +-- =========================== +-- products: nowe kolumny danych produktu +-- =========================== + +SET @sql = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'products' + AND COLUMN_NAME = 'custom_label_4' + ), + 'DO 1', + 'ALTER TABLE `products` ADD COLUMN `custom_label_4` VARCHAR(255) NULL DEFAULT NULL AFTER `min_roas`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'products' + AND COLUMN_NAME = 'custom_label_3' + ), + 'DO 1', + 'ALTER TABLE `products` ADD COLUMN `custom_label_3` VARCHAR(255) NULL DEFAULT NULL AFTER `custom_label_4`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'products' + AND COLUMN_NAME = 'title' + ), + 'DO 1', + 'ALTER TABLE `products` ADD COLUMN `title` VARCHAR(255) NULL DEFAULT NULL AFTER `custom_label_3`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'products' + AND COLUMN_NAME = 'description' + ), + 'DO 1', + 'ALTER TABLE `products` ADD COLUMN `description` TEXT NULL AFTER `title`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'products' + AND COLUMN_NAME = 'google_product_category' + ), + 'DO 1', + 'ALTER TABLE `products` ADD COLUMN `google_product_category` TEXT NULL AFTER `description`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'products' + AND COLUMN_NAME = 'product_url' + ), + 'DO 1', + 'ALTER TABLE `products` ADD COLUMN `product_url` VARCHAR(500) NULL DEFAULT NULL AFTER `google_product_category`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- =========================== +-- Backfill products <- products_data +-- (zostawiamy products_data na razie dla kompatybilnosci) +-- =========================== + +UPDATE `products` p +INNER JOIN `products_data` pd ON pd.product_id = p.id +SET + p.custom_label_4 = CASE + WHEN ( p.custom_label_4 IS NULL OR TRIM( p.custom_label_4 ) = '' ) AND pd.custom_label_4 IS NOT NULL AND TRIM( pd.custom_label_4 ) <> '' THEN pd.custom_label_4 + ELSE p.custom_label_4 + END, + p.custom_label_3 = CASE + WHEN ( p.custom_label_3 IS NULL OR TRIM( p.custom_label_3 ) = '' ) AND pd.custom_label_3 IS NOT NULL AND TRIM( pd.custom_label_3 ) <> '' THEN pd.custom_label_3 + ELSE p.custom_label_3 + END, + p.title = CASE + WHEN ( p.title IS NULL OR TRIM( p.title ) = '' ) AND pd.title IS NOT NULL AND TRIM( pd.title ) <> '' THEN pd.title + ELSE p.title + END, + p.description = CASE + WHEN ( p.description IS NULL OR TRIM( p.description ) = '' ) AND pd.description IS NOT NULL AND TRIM( pd.description ) <> '' THEN pd.description + ELSE p.description + END, + p.google_product_category = CASE + WHEN ( p.google_product_category IS NULL OR TRIM( p.google_product_category ) = '' ) AND pd.google_product_category IS NOT NULL AND TRIM( pd.google_product_category ) <> '' THEN pd.google_product_category + ELSE p.google_product_category + END, + p.product_url = CASE + WHEN ( p.product_url IS NULL OR TRIM( p.product_url ) = '' ) AND pd.product_url IS NOT NULL AND TRIM( pd.product_url ) <> '' THEN pd.product_url + ELSE p.product_url + END; + +-- =========================== +-- Tabela agregatow produktow +-- =========================== + +CREATE TABLE IF NOT EXISTS `products_aggregate` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `product_id` INT(11) NOT NULL, + `campaign_id` INT(11) NOT NULL DEFAULT 0, + `ad_group_id` INT(11) NOT NULL DEFAULT 0, + `impressions_30` INT(11) NOT NULL DEFAULT 0, + `clicks_30` INT(11) NOT NULL DEFAULT 0, + `ctr_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000, + `cost_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000, + `conversions_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000, + `conversion_value_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000, + `roas_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000, + `impressions_all_time` INT(11) NOT NULL DEFAULT 0, + `clicks_all_time` INT(11) NOT NULL DEFAULT 0, + `ctr_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000, + `cost_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000, + `conversions_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000, + `conversion_value_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000, + `roas_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000, + `date_sync` DATE NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_products_aggregate_scope` (`product_id`,`campaign_id`,`ad_group_id`), + KEY `idx_products_aggregate_campaign_id` (`campaign_id`), + KEY `idx_products_aggregate_ad_group_id` (`ad_group_id`), + KEY `idx_products_aggregate_date_sync` (`date_sync`), + CONSTRAINT `FK_products_aggregate_products` + FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/migrations/017_drop_products_data.sql b/migrations/017_drop_products_data.sql new file mode 100644 index 0000000..2f70ba1 --- /dev/null +++ b/migrations/017_drop_products_data.sql @@ -0,0 +1,5 @@ +-- Migracja: usuniecie tabeli products_data +-- Dane produktowe sa juz przechowywane bezposrednio w tabeli products. + +DROP TABLE IF EXISTS `products_data`; + diff --git a/migrations/018_products_merchant_url_flags.sql b/migrations/018_products_merchant_url_flags.sql new file mode 100644 index 0000000..0d4f87f --- /dev/null +++ b/migrations/018_products_merchant_url_flags.sql @@ -0,0 +1,38 @@ +-- Flagi kontroli pobierania URL z Merchant Center + +SET @sql = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'products' + AND COLUMN_NAME = 'merchant_url_not_found' + ), + 'DO 1', + 'ALTER TABLE `products` ADD COLUMN `merchant_url_not_found` TINYINT(1) NOT NULL DEFAULT 0 AFTER `product_url`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'products' + AND COLUMN_NAME = 'merchant_url_last_check' + ), + 'DO 1', + 'ALTER TABLE `products` ADD COLUMN `merchant_url_last_check` DATETIME NULL DEFAULT NULL AFTER `merchant_url_not_found`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Normalizacja: puste/sztuczne wartosci URL traktujemy jako brak URL. +UPDATE `products` +SET `product_url` = NULL +WHERE TRIM( COALESCE( `product_url`, '' ) ) = '' + OR LOWER( TRIM( `product_url` ) ) IN ( '0', '-', 'null' ); + diff --git a/migrations/019_campaign_alerts_product_id.sql b/migrations/019_campaign_alerts_product_id.sql new file mode 100644 index 0000000..07f1cec --- /dev/null +++ b/migrations/019_campaign_alerts_product_id.sql @@ -0,0 +1,31 @@ +-- Powiazanie alertow z konkretnym produktem + +SET @sql = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'campaign_alerts' + AND COLUMN_NAME = 'product_id' + ), + 'DO 1', + 'ALTER TABLE `campaign_alerts` ADD COLUMN `product_id` INT(11) NULL DEFAULT NULL AFTER `ad_group_external_id`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'campaign_alerts' + AND INDEX_NAME = 'idx_alert_product' + ), + 'DO 1', + 'CREATE INDEX `idx_alert_product` ON `campaign_alerts` (`product_id`)' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/migrations/demo_data.sql b/migrations/demo_data.sql index c31c0bf..bc3feb7 100644 --- a/migrations/demo_data.sql +++ b/migrations/demo_data.sql @@ -182,18 +182,6 @@ WHERE p.client_id = 2 'shopify_PL_8901021','shopify_PL_8901022','shopify_PL_8901023','shopify_PL_8901024','shopify_PL_8901025' ); -DELETE pd -FROM products_data pd -JOIN products p ON p.id = pd.product_id -WHERE p.client_id = 2 - AND p.offer_id IN ( - 'shopify_PL_8901001','shopify_PL_8901002','shopify_PL_8901003','shopify_PL_8901004','shopify_PL_8901005', - 'shopify_PL_8901006','shopify_PL_8901007','shopify_PL_8901008','shopify_PL_8901009','shopify_PL_8901010', - 'shopify_PL_8901011','shopify_PL_8901012','shopify_PL_8901013','shopify_PL_8901014','shopify_PL_8901015', - 'shopify_PL_8901016','shopify_PL_8901017','shopify_PL_8901018','shopify_PL_8901019','shopify_PL_8901020', - 'shopify_PL_8901021','shopify_PL_8901022','shopify_PL_8901023','shopify_PL_8901024','shopify_PL_8901025' - ); - DELETE ph FROM products_history ph JOIN products p ON p.id = ph.product_id @@ -387,26 +375,17 @@ GROUP BY p.id, p.name; -- ============================================================ --- 6. PRODUCTS_DATA (custom labels) +-- 6. PRODUCTS (custom labels) -- ============================================================ -- Bestsellery (wysoki ROAS + dużo konwersji) -INSERT INTO products_data (product_id, custom_label_4) VALUES -((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901001' AND client_id = 2), 'bestseller'), -((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901003' AND client_id = 2), 'bestseller'), -((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901010' AND client_id = 2), 'bestseller'), -((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901023' AND client_id = 2), 'bestseller'); +UPDATE products SET custom_label_4 = 'bestseller' WHERE offer_id IN ( 'shopify_PL_8901001', 'shopify_PL_8901003', 'shopify_PL_8901010', 'shopify_PL_8901023' ) AND client_id = 2; -- Produkty PLA (w kampaniach Shopping) -INSERT INTO products_data (product_id, custom_label_4) VALUES -((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901005' AND client_id = 2), 'pla'), -((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901006' AND client_id = 2), 'pla'), -((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901015' AND client_id = 2), 'pla'); +UPDATE products SET custom_label_4 = 'pla' WHERE offer_id IN ( 'shopify_PL_8901005', 'shopify_PL_8901006', 'shopify_PL_8901015' ) AND client_id = 2; -- Zombie (bardzo niskie wyświetlenia) -INSERT INTO products_data (product_id, custom_label_4) VALUES -((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901014' AND client_id = 2), 'zombie'), -((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901024' AND client_id = 2), 'zombie'); +UPDATE products SET custom_label_4 = 'zombie' WHERE offer_id IN ( 'shopify_PL_8901014', 'shopify_PL_8901024' ) AND client_id = 2; -- ============================================================ diff --git a/templates/campaign_alerts/main_view.php b/templates/campaign_alerts/main_view.php index 26fcd1f..f8bdef7 100644 --- a/templates/campaign_alerts/main_view.php +++ b/templates/campaign_alerts/main_view.php @@ -17,97 +17,185 @@ -
- - - - - - - - - - - - alerts ) ): ?> + + + + +
+ +
+ +
+
DataKlientKampaniaGrupa reklamKomunikat
+ - + + + + + + - - alerts as $row ): ?> - + + + alerts ) ): ?> - - - - - + - - - -
Brak alertów. + + DataKlientKampaniaGrupa reklamKomunikat
Brak alertow.
+ + alerts as $row ): ?> + total_pages > 1 ): ?> -
-
- +
-
- - + + + + + diff --git a/templates/clients/main_view.php b/templates/clients/main_view.php index 21bcfed..344ffa4 100644 --- a/templates/clients/main_view.php +++ b/templates/clients/main_view.php @@ -12,19 +12,28 @@ #ID Nazwa klienta + Status Google Ads Customer ID Merchant Account ID Dane od - Sync - Akcje + Sync + Akcje clients ): ?> clients as $client ): ?> - + + + + + Aktywny + + Nieaktywny + + @@ -48,13 +57,21 @@ + - - + + + +
@@ -39,6 +44,26 @@
+
+ + + Alerty dla wybranej kampanii (i opcjonalnie grupy reklam) + (0) + +
+
+ +
+ + + Produkty do sprawdzenia (0 wyswietlen w ostatnich 30 dniach) + (0) + +
+ +
+
+
+ @@ -382,6 +383,12 @@ { last_invoked.textContent = row && row.last_invoked_at ? row.last_invoked_at : 'Brak danych'; } + + var plan = container.querySelector( '[data-cron-url-plan]' ); + if ( plan ) + { + plan.textContent = row && row.plan ? row.plan : ''; + } } ); } diff --git a/tmp/campaign_alerts_debug.log b/tmp/campaign_alerts_debug.log new file mode 100644 index 0000000..78e9397 --- /dev/null +++ b/tmp/campaign_alerts_debug.log @@ -0,0 +1,54 @@ +{"ts":"2026-02-20 10:51:13","stage":"sync_product_campaign_alerts_for_client:start","context":{"client_id":2,"customer_id":"941-605-1782","date_sync":"2026-02-20","campaigns_db_map_count":6,"ad_group_db_map_count":20}} +{"ts":"2026-02-20 10:51:15","stage":"google_ads:get_shopping_ad_group_offer_ids","context":{"result_type":"false","rows_count":0,"sample":[]}} +{"ts":"2026-02-20 10:51:16","stage":"google_ads:get_shopping_ad_group_offer_ids_from_performance","context":{"result_type":"array","rows_count":147,"sample":[{"campaign_id":22389325355,"campaign_name":"[PLA] chrzest święty","ad_group_id":174195978930,"ad_group_name":"Pamiątka Pierwszej Komunii Świętej Pudełko na pieniądze - Kielich","offer_ids":[1773]},{"campaign_id":22389325355,"campaign_name":"[PLA] chrzest święty","ad_group_id":175963483182,"ad_group_name":"Pamiątka Pierwszej Komunii Świętej Pudełko na pieniądze - Dłonie","offer_ids":[1772]},{"campaign_id":22389325355,"campaign_name":"[PLA] chrzest święty","ad_group_id":177111077493,"ad_group_name":"Prośba o bycie Matką Chrzestną, grawerowana, z imieniem dziecka, Misiek - Kolor serduszka: czerwony","offer_ids":[921]}]}} +{"ts":"2026-02-20 10:51:16","stage":"merchant:offer_ids_to_verify","context":{"count":41,"sample":[1675,1467,1063,1678,910,1500,944,1652,1738,1072,1672,2025,934,928,1316,1319,1313,1715,1677,1767,1340,1778,1067,1721,1368,1339,949,1345,1824,1536]}} +{"ts":"2026-02-20 10:51:18","stage":"merchant:get_merchant_products_for_offer_ids","context":{"merchant_account_id":"729206752","request_count":41,"response_type":"array","response_count":41,"response_sample_keys":[1467,1767,1672,1652,1067,1368,1510,1675,1824,1536,1321,1340,1720,1063,1339,1316,949,1436,1715,1722,1778,1874,1723,928,1319,1345,1313,1738,1677,1678]}} +{"ts":"2026-02-20 10:51:18","stage":"db:local_offer_ids","context":{"count":592,"sample":[1778,2013,944,1536,2016,1928,2014,1720,1951,1064,1978,906,921,1993,1942,1812,1813,1061,1596,1509,911,1360,1569,1662,2080,1581,1783,1580,2071,1849]}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|178830027730","campaign_external_id":22855767992,"ad_group_external_id":178830027730,"offer_ids":["1675"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|182023998383","campaign_external_id":22855767992,"ad_group_external_id":182023998383,"offer_ids":["1467"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|182184163854","campaign_external_id":22855767992,"ad_group_external_id":182184163854,"offer_ids":["1063"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|182329510303","campaign_external_id":22881177775,"ad_group_external_id":182329510303,"offer_ids":["1678"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|182437144463","campaign_external_id":22855767992,"ad_group_external_id":182437144463,"offer_ids":["910"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|183091891829","campaign_external_id":22855767992,"ad_group_external_id":183091891829,"offer_ids":["1500"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|183523029996","campaign_external_id":22855767992,"ad_group_external_id":183523029996,"offer_ids":["944"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|184090691740","campaign_external_id":22855767992,"ad_group_external_id":184090691740,"offer_ids":["1652"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|184491635422","campaign_external_id":22881177775,"ad_group_external_id":184491635422,"offer_ids":["1738"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|184781679245","campaign_external_id":22881177775,"ad_group_external_id":184781679245,"offer_ids":["1072"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|185056281737","campaign_external_id":22881177775,"ad_group_external_id":185056281737,"offer_ids":["1672"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|186478211005","campaign_external_id":22881177775,"ad_group_external_id":186478211005,"offer_ids":["2025"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|187193909207","campaign_external_id":22855767992,"ad_group_external_id":187193909207,"offer_ids":["934"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|187202566030","campaign_external_id":22855767992,"ad_group_external_id":187202566030,"offer_ids":["928"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|187617501346","campaign_external_id":22881177775,"ad_group_external_id":187617501346,"offer_ids":["1316"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|187617773426","campaign_external_id":22881177775,"ad_group_external_id":187617773426,"offer_ids":["1319"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|187737512843","campaign_external_id":22881177775,"ad_group_external_id":187737512843,"offer_ids":["1313"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|188437646135","campaign_external_id":22881177775,"ad_group_external_id":188437646135,"offer_ids":["1715"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|188844670995","campaign_external_id":22881177775,"ad_group_external_id":188844670995,"offer_ids":["1677"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23169295644|189029743564","campaign_external_id":23169295644,"ad_group_external_id":189029743564,"offer_ids":["1767"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|189133168538","campaign_external_id":22881177775,"ad_group_external_id":189133168538,"offer_ids":["1340"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|189174321780","campaign_external_id":22881177775,"ad_group_external_id":189174321780,"offer_ids":["1672"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|189377483261","campaign_external_id":22855767992,"ad_group_external_id":189377483261,"offer_ids":["1778"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|189393194528","campaign_external_id":22855767992,"ad_group_external_id":189393194528,"offer_ids":["1067"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|189782782720","campaign_external_id":22881177775,"ad_group_external_id":189782782720,"offer_ids":["1721"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|190157433529","campaign_external_id":22881177775,"ad_group_external_id":190157433529,"offer_ids":["1368"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|190254577916","campaign_external_id":22855767992,"ad_group_external_id":190254577916,"offer_ids":["1738"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|190981545071","campaign_external_id":22881177775,"ad_group_external_id":190981545071,"offer_ids":["1339"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|191005617337","campaign_external_id":23554819089,"ad_group_external_id":191005617337,"offer_ids":["944"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|191005617537","campaign_external_id":23554819089,"ad_group_external_id":191005617537,"offer_ids":["949"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|191005617577","campaign_external_id":23554819089,"ad_group_external_id":191005617577,"offer_ids":["1345"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|191005617737","campaign_external_id":23554819089,"ad_group_external_id":191005617737,"offer_ids":["1824"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|191005618937","campaign_external_id":23554819089,"ad_group_external_id":191005618937,"offer_ids":["1536"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|191170651834","campaign_external_id":22881177775,"ad_group_external_id":191170651834,"offer_ids":["1324"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|191185839027","campaign_external_id":22881177775,"ad_group_external_id":191185839027,"offer_ids":["1321"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|191596793445","campaign_external_id":22855767992,"ad_group_external_id":191596793445,"offer_ids":["2016"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|191936594094","campaign_external_id":23554819089,"ad_group_external_id":191936594094,"offer_ids":["1928"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|191938697986","campaign_external_id":22855767992,"ad_group_external_id":191938697986,"offer_ids":["949"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|192480293437","campaign_external_id":22855767992,"ad_group_external_id":192480293437,"offer_ids":["1536"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|192764842295","campaign_external_id":22855767992,"ad_group_external_id":192764842295,"offer_ids":["1824"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|193141002373","campaign_external_id":23554819089,"ad_group_external_id":193141002373,"offer_ids":["1915"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|193760921208","campaign_external_id":22881177775,"ad_group_external_id":193760921208,"offer_ids":["1723"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|194005981900","campaign_external_id":23554819089,"ad_group_external_id":194005981900,"offer_ids":["1874"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|194928085664","campaign_external_id":22881177775,"ad_group_external_id":194928085664,"offer_ids":["1722"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|195763522708","campaign_external_id":22881177775,"ad_group_external_id":195763522708,"offer_ids":["1436"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|197096047150","campaign_external_id":22855767992,"ad_group_external_id":197096047150,"offer_ids":["1720"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|198928421011","campaign_external_id":23554819089,"ad_group_external_id":198928421011,"offer_ids":["1510"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}} +{"ts":"2026-02-20 10:51:18","stage":"alerts:cleanup","context":{"existing_count":7,"cleaned_count":0,"problematic_without_active_product":0,"problematic_with_orphaned_offers":0,"problematic_without_detected_product":7}} diff --git a/tmp/debug_clients.php b/tmp/debug_clients.php new file mode 100644 index 0000000..bccc0e2 --- /dev/null +++ b/tmp/debug_clients.php @@ -0,0 +1,32 @@ + 'mysql', + 'database_name' => $database['name'], + 'server' => $database['host'], + 'username' => $database['user'], + 'password' => $database['password'], + 'charset' => 'utf8' + ]); + echo "CONNECTED\n"; + $cols = $mdb->query("SHOW COLUMNS FROM clients")->fetchAll(PDO::FETCH_ASSOC); + echo "COLUMNS=" . count($cols) . "\n"; + foreach ($cols as $c) { + echo ($c['Field'] ?? '') . "\n"; + } + $data = $mdb->query("SELECT id, name, active, COALESCE(google_ads_customer_id,'') AS google_ads_customer_id FROM clients ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC); + echo "ROWS=" . count($data) . "\n"; + foreach ($data as $r) { + echo json_encode($r, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . "\n"; + } + $eligible = $mdb->query("SELECT id FROM clients WHERE COALESCE(deleted,0)=0 AND active=1 AND google_ads_customer_id IS NOT NULL AND google_ads_customer_id<>'' ORDER BY id ASC")->fetchAll(PDO::FETCH_COLUMN); + echo "ELIGIBLE=" . json_encode($eligible) . "\n"; +} catch (Throwable $e) { + echo "ERR: " . $e->getMessage() . "\n"; +} +?> diff --git a/tmp/debug_clients_remote.php b/tmp/debug_clients_remote.php new file mode 100644 index 0000000..9650162 --- /dev/null +++ b/tmp/debug_clients_remote.php @@ -0,0 +1,35 @@ + 'mysql', + 'database_name' => $database['name'], + 'server' => $host, + 'username' => $database['user'], + 'password' => $database['password'], + 'charset' => 'utf8' + ]); + + echo "CONNECTED_REMOTE=" . $host . PHP_EOL; + + $rows = $mdb->query("SELECT id, name, COALESCE(active,0) AS active, COALESCE(deleted,0) AS deleted, CONCAT('[', COALESCE(google_ads_customer_id,''), ']') AS google_ads_customer_id FROM clients ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC); + echo "CLIENTS_TOTAL=" . count($rows) . PHP_EOL; + foreach ($rows as $r) { + echo json_encode($r, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . PHP_EOL; + } + + $eligible = $mdb->query("SELECT id, name FROM clients WHERE COALESCE(deleted,0)=0 AND COALESCE(active,0)=1 AND TRIM(COALESCE(google_ads_customer_id,''))<>'' ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC); + echo "ELIGIBLE_TOTAL=" . count($eligible) . PHP_EOL; + foreach ($eligible as $e) { + echo "ELIGIBLE=" . json_encode($e, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . PHP_EOL; + } + +} catch (Throwable $e) { + echo "ERR=" . $e->getMessage() . PHP_EOL; +} +?> diff --git a/tmp/debug_clients_remote2.php b/tmp/debug_clients_remote2.php new file mode 100644 index 0000000..939bdf4 --- /dev/null +++ b/tmp/debug_clients_remote2.php @@ -0,0 +1,20 @@ + 'mysql', + 'database_name' => $database['name'], + 'server' => $database['remote_host'], + 'username' => $database['user'], + 'password' => $database['password'], + 'charset' => 'utf8' +]); +$cols = $mdb->query("SHOW COLUMNS FROM clients")->fetchAll(PDO::FETCH_ASSOC); +echo "COLUMNS:" . PHP_EOL; +foreach ($cols as $c) { echo $c['Field'] . PHP_EOL; } +$rows = $mdb->query("SELECT id, name, COALESCE(active,0) AS active, CONCAT('[', COALESCE(google_ads_customer_id,''), ']') AS google_ads_customer_id FROM clients ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC); +echo "ROWS=" . count($rows) . PHP_EOL; +foreach ($rows as $r) { echo json_encode($r, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . PHP_EOL; } +$eligible = $mdb->query("SELECT id, name FROM clients WHERE COALESCE(active,0)=1 AND TRIM(COALESCE(google_ads_customer_id,''))<>'' ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC); +echo "ELIGIBLE=" . json_encode($eligible, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . PHP_EOL; +?> diff --git a/tmp/debug_eligible_remote.php b/tmp/debug_eligible_remote.php new file mode 100644 index 0000000..db0241f --- /dev/null +++ b/tmp/debug_eligible_remote.php @@ -0,0 +1,14 @@ + 'mysql', + 'database_name' => $database['name'], + 'server' => $database['remote_host'], + 'username' => $database['user'], + 'password' => $database['password'], + 'charset' => 'utf8' +]); +$rows = $mdb->query("SELECT id, name FROM clients WHERE COALESCE(active,0)=1 AND TRIM(COALESCE(google_ads_customer_id,''))<>'' ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC); +echo json_encode($rows, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . PHP_EOL; +?>