get( 'users', '*', [ 'id' => $user[ 'id' ] ] ); \S::set_session( 'user', $user ); \S::alert( 'Ustawienia zostały zapisane.' ); } header( 'Location: /settings' ); exit; } public static function settings() { global $user; if ( !$user ) { header( 'Location: /login' ); exit; } return \view\Users::settings( $user, self::get_cron_dashboard_data() ); } public static function settings_cron_status() { global $user; header( 'Content-Type: application/json; charset=utf-8' ); header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' ); header( 'Pragma: no-cache' ); if ( !$user ) { http_response_code( 403 ); echo json_encode( [ 'status' => 'error', 'message' => 'Brak autoryzacji.' ] ); exit; } echo json_encode( [ 'status' => 'ok', 'data' => self::get_cron_dashboard_data() ], JSON_UNESCAPED_UNICODE ); exit; } public static function settings_save_google_ads() { $fields = [ 'google_ads_developer_token', 'google_ads_client_id', 'google_ads_client_secret', 'google_ads_refresh_token', 'google_merchant_refresh_token', 'google_ads_manager_account_id', ]; foreach ( $fields as $field ) { \services\GoogleAdsApi::set_setting( $field, \S::get( $field ) ); } \services\GoogleAdsApi::set_setting( 'google_ads_debug_enabled', \S::get( 'google_ads_debug_enabled' ) ? '1' : '0' ); // wyczyść cached token przy zmianie credentials \services\GoogleAdsApi::set_setting( 'google_ads_access_token', null ); \services\GoogleAdsApi::set_setting( 'google_ads_access_token_expires', null ); \services\GoogleAdsApi::set_setting( 'google_merchant_access_token', null ); \services\GoogleAdsApi::set_setting( 'google_merchant_access_token_expires', null ); \S::alert( 'Ustawienia Google Ads zostały zapisane.' ); header( 'Location: /settings' ); exit; } public static function settings_save_openai() { \services\GoogleAdsApi::set_setting( 'openai_enabled', \S::get( 'openai_enabled' ) ? '1' : '0' ); \services\GoogleAdsApi::set_setting( 'openai_api_key', \S::get( 'openai_api_key' ) ); \services\GoogleAdsApi::set_setting( 'openai_model', \S::get( 'openai_model' ) ); \S::alert( 'Ustawienia OpenAI zostały zapisane.' ); header( 'Location: /settings' ); exit; } public static function settings_save_claude() { \services\GoogleAdsApi::set_setting( 'claude_enabled', \S::get( 'claude_enabled' ) ? '1' : '0' ); \services\GoogleAdsApi::set_setting( 'claude_api_key', \S::get( 'claude_api_key' ) ); \services\GoogleAdsApi::set_setting( 'claude_model', \S::get( 'claude_model' ) ); \S::alert( 'Ustawienia Claude zostały zapisane.' ); header( 'Location: /settings' ); exit; } private static function get_cron_dashboard_data() { 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(); $campaign_window_state = self::get_setting_json( 'cron_campaigns_window_state' ); $campaign_daily_state = self::get_setting_json( 'cron_campaigns_state' ); $campaign_dates = self::normalize_dates( $campaign_window_state['sync_dates'] ?? [] ); $campaign_dates_count = count( $campaign_dates ); if ( $campaign_dates_count < 1 ) { $campaign_dates = [ date( 'Y-m-d' ) ]; $campaign_dates_count = 1; } $campaign_current_date_index = (int) ( $campaign_window_state['current_date_index'] ?? 0 ); $campaign_current_date_index = max( 0, min( $campaign_dates_count - 1, $campaign_current_date_index ) ); $campaign_processed_today = count( self::normalize_ids( $campaign_daily_state['processed_ids'] ?? [] ) ); $campaign_processed_today = min( $clients_total, $campaign_processed_today ); $campaign_total = $clients_total * $campaign_dates_count; $campaign_processed = min( $campaign_total, ( $campaign_current_date_index * $clients_total ) + $campaign_processed_today ); $campaign_remaining = max( 0, $campaign_total - $campaign_processed ); $campaign_active_date = $campaign_window_state['sync_date'] ?? ( $campaign_dates[ $campaign_current_date_index ] ?? '' ); $campaign_meta = 'Aktywny dzień: ' . ( $campaign_active_date ?: '-' ) . ', okno dni: ' . $campaign_dates_count; $campaign_eta_meta = self::build_eta_meta( 'cron_campaigns', $campaign_remaining ); if ( $campaign_eta_meta !== '' ) { $campaign_meta .= ', ' . $campaign_eta_meta; } $products_state = self::get_setting_json( 'cron_products_pipeline_state' ); $products_dates = self::normalize_dates( $products_state['import_dates'] ?? [] ); $products_dates_count = count( $products_dates ); if ( $products_dates_count < 1 ) { $products_dates = [ date( 'Y-m-d' ) ]; $products_dates_count = 1; } $products_current_date_index = (int) ( $products_state['current_date_index'] ?? 0 ); $products_current_date_index = max( 0, min( $products_dates_count - 1, $products_current_date_index ) ); $products_phase = (string) ( $products_state['phase'] ?? 'fetch' ); $products_fetch_done = count( self::normalize_ids( $products_state['fetch_done_ids'] ?? [] ) ); $products_aggregate_30_done = count( self::normalize_ids( $products_state['aggregate_30_done_ids'] ?? [] ) ); $products_aggregate_temp_done = count( self::normalize_ids( $products_state['aggregate_temp_done_ids'] ?? [] ) ); $products_fetch_done = min( $clients_total, $products_fetch_done ); $products_aggregate_30_done = min( $clients_total, $products_aggregate_30_done ); $products_aggregate_temp_done = min( $clients_total, $products_aggregate_temp_done ); $products_per_day_total = $clients_total * 3; $products_total = $products_per_day_total * $products_dates_count; $products_done_in_day = 0; if ( $products_phase === 'aggregate_30' ) { $products_done_in_day = $clients_total + $products_aggregate_30_done; } else if ( $products_phase === 'aggregate_temp' ) { $products_done_in_day = ( $clients_total * 2 ) + $products_aggregate_temp_done; } else if ( $products_phase === 'done' ) { $products_done_in_day = $products_per_day_total; } else { $products_done_in_day = $products_fetch_done; } $products_done_in_day = min( $products_per_day_total, $products_done_in_day ); $products_processed = min( $products_total, ( $products_current_date_index * $products_per_day_total ) + $products_done_in_day ); if ( $products_phase === 'done' ) { $products_processed = $products_total; } $products_remaining = max( 0, $products_total - $products_processed ); $products_phase_labels = [ 'fetch' => 'Pobieranie', 'aggregate_30' => 'Agregacja 30 dni', 'aggregate_temp' => 'Agregacja temp', 'done' => 'Zakończono' ]; $products_phase_label = $products_phase_labels[ $products_phase ] ?? $products_phase; $products_active_date = $products_state['import_date'] ?? ( $products_dates[ $products_current_date_index ] ?? '' ); $products_meta = 'Faza: ' . $products_phase_label . ', aktywny dzień: ' . ( $products_active_date ?: '-' ) . ', okno dni: ' . $products_dates_count; $products_eta_meta = self::build_eta_meta( 'cron_products', $products_remaining ); if ( $products_eta_meta !== '' ) { $products_meta .= ', ' . $products_eta_meta; } $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' ], ]; $urls = []; foreach ( $cron_endpoints as $endpoint ) { $last_key = 'cron_last_invoked_' . $endpoint['action'] . '_at'; $urls[] = [ 'name' => $endpoint['name'], 'url' => $base_url . $endpoint['path'], 'last_invoked_at' => self::format_datetime( \services\GoogleAdsApi::get_setting( $last_key ) ), ]; } return [ 'overall_last_invoked_at' => self::format_datetime( \services\GoogleAdsApi::get_setting( 'cron_last_invoked_at' ) ), 'clients_total' => $clients_total, 'progress' => [ [ 'name' => 'Kampanie', 'processed' => $campaign_processed, 'total' => $campaign_total, 'percent' => self::progress_percent( $campaign_processed, $campaign_total ), 'meta' => $campaign_meta ], [ 'name' => 'Produkty', 'processed' => $products_processed, 'total' => $products_total, 'percent' => self::progress_percent( $products_processed, $products_total ), 'meta' => $products_meta ], ], 'urls' => $urls ]; } private static function get_setting_json( $setting_key ) { $raw = \services\GoogleAdsApi::get_setting( $setting_key ); if ( !$raw ) { return []; } $decoded = json_decode( (string) $raw, true ); return is_array( $decoded ) ? $decoded : []; } private static function normalize_ids( $items ) { $result = []; foreach ( (array) $items as $item ) { $id = (int) $item; if ( $id > 0 ) { $result[] = $id; } } return array_values( array_unique( $result ) ); } private static function normalize_dates( $items ) { $result = []; foreach ( (array) $items as $item ) { $timestamp = strtotime( (string) $item ); if ( !$timestamp ) { continue; } $result[] = date( 'Y-m-d', $timestamp ); } $result = array_values( array_unique( $result ) ); sort( $result ); return $result; } private static function progress_percent( $processed, $total ) { $processed = (int) $processed; $total = (int) $total; if ( $total <= 0 ) { return 0; } return (int) round( min( 100, max( 0, ( $processed / $total ) * 100 ) ) ); } private static function build_eta_meta( $action_name, $remaining_tasks ) { $remaining_tasks = max( 0, (int) $remaining_tasks ); if ( $remaining_tasks <= 0 ) { return 'Szacowany koniec: zakończono'; } $avg_interval_seconds = (float) \services\GoogleAdsApi::get_setting( 'cron_avg_interval_' . $action_name . '_sec' ); $last_interval_seconds = (int) \services\GoogleAdsApi::get_setting( 'cron_last_interval_' . $action_name . '_sec' ); if ( $avg_interval_seconds <= 0 && $last_interval_seconds > 0 ) { $avg_interval_seconds = (float) $last_interval_seconds; } if ( $avg_interval_seconds <= 0 ) { return 'Szacowany koniec: brak danych o częstotliwości'; } $estimated_seconds = (int) max( 1, round( $remaining_tasks * $avg_interval_seconds ) ); $eta_timestamp = time() + $estimated_seconds; return 'Śr. interwał: ' . self::format_duration_short( (int) round( $avg_interval_seconds ) ) . ', szacowany koniec: ' . date( 'Y-m-d H:i:s', $eta_timestamp ) . ' (za ' . self::format_duration_short( $estimated_seconds ) . ')'; } private static function format_duration_short( $seconds ) { $seconds = max( 0, (int) $seconds ); if ( $seconds < 60 ) { return $seconds . ' sek'; } $days = (int) floor( $seconds / 86400 ); $seconds -= $days * 86400; $hours = (int) floor( $seconds / 3600 ); $seconds -= $hours * 3600; $minutes = (int) floor( $seconds / 60 ); $parts = []; if ( $days > 0 ) { $parts[] = $days . ' d'; } if ( $hours > 0 ) { $parts[] = $hours . ' h'; } if ( $minutes > 0 ) { $parts[] = $minutes . ' min'; } if ( empty( $parts ) ) { return '1 min'; } return implode( ' ', array_slice( $parts, 0, 2 ) ); } private static function format_datetime( $value ) { $timestamp = strtotime( (string) $value ); if ( !$timestamp ) { return 'Brak danych'; } return date( 'Y-m-d H:i:s', $timestamp ); } private static function get_base_url() { $scheme = ( !empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] !== 'off' ) ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? ( $_SERVER['SERVER_NAME'] ?? 'localhost' ); return $scheme . '://' . $host; } public static function login() { if ( $user = \factory\Users::login( \S::get( 'email' ), md5( \S::get( 'password' ) ) ) ) { // zapamiętaj logowanie if ( \S::get( 'remember' ) ) { $domain = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] ); $cookie_name = str_replace( '.', '-', $domain ); $value = [ 'email' => \S::get( 'email' ), 'hash' => md5( \S::get( 'password' ) ) ]; $value = json_encode( $value ); setcookie( $cookie_name, $value, strtotime( "+1 year" ), "/", $domain ); } \S::set_session( 'user', $user ); echo json_encode( [ 'result' => 'true', 'msg' => 'Właśnie zostałeś zalogowany. Za chwilę nastąpi przekierowanie.', 'default_project' => $user[ 'default_project' ] ] ); } else { echo json_encode( [ 'result' => 'false', 'msg' => 'Podany login i hasło są nieprawidłowe.' ] ); } exit; } public static function login_form() { return \Tpl::view( 'users/login-form' ); } }