0 ) { return min( 90, $settings_value ); } $config_value = (int) ( $settings['facebook_ads_conversion_window_days'] ?? 30 ); if ( $config_value <= 0 ) { $config_value = 30; } return min( 90, $config_value ); } static private function normalize_facebook_ads_account_id( $value ) { $value = trim( (string) $value ); if ( $value === '' ) { return null; } if ( stripos( $value, 'act_' ) === 0 ) { $digits = preg_replace( '/\D+/', '', substr( $value, 4 ) ); return $digits !== '' ? 'act_' . $digits : null; } $digits = preg_replace( '/\D+/', '', $value ); if ( $digits === '' ) { return null; } return 'act_' . $digits; } 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( \factory\Clients::get_all() ); } static public function save() { $id = \S::get( 'id' ); $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' ) ); $facebook_ads_account_id = self::normalize_facebook_ads_account_id( \S::get( 'facebook_ads_account_id' ) ); $active_raw = \S::get( 'active' ); $active = (string) $active_raw === '0' ? 0 : 1; if ( !$name ) { \S::alert( 'Nazwa klienta jest wymagana.' ); header( 'Location: /clients' ); exit; } $google_ads_start_date = trim( \S::get( 'google_ads_start_date' ) ); $data = [ 'name' => $name, 'google_ads_customer_id' => $google_ads_customer_id ?: null, 'google_merchant_account_id' => $google_merchant_account_id ?: null, 'facebook_ads_account_id' => $facebook_ads_account_id, 'google_ads_start_date' => $google_ads_start_date ?: null, 'active' => $active, ]; if ( $id ) { \factory\Clients::update( $id, $data ); \S::alert( 'Klient został zaktualizowany.' ); } else { \factory\Clients::create( $data ); \S::alert( 'Klient został dodany.' ); } header( 'Location: /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' ); if ( $id ) { \factory\Clients::delete( $id ); } echo json_encode( [ 'success' => true ] ); exit; } static public function get() { $id = \S::get( 'id' ); $client = \factory\Clients::get( $id ); echo json_encode( $client ?: [] ); exit; } static public function sync_status() { global $mdb; $clients_not_deleted_sql = self::sql_clients_not_deleted(); $clients_not_deleted_sql_c = self::sql_clients_not_deleted( 'c' ); // Kampanie: 1 work unit per row (pending=0, done=1) $campaigns_raw = $mdb->query( "SELECT client_id, COUNT(*) as total, SUM(CASE WHEN phase='done' THEN 1 ELSE 0 END) as done FROM cron_sync_status WHERE pipeline='campaigns' GROUP BY client_id" )->fetchAll( \PDO::FETCH_ASSOC ); // Produkty: 3 work units per row (pending=0, fetch=1, aggregate_30=2, done=3) $products_raw = $mdb->query( "SELECT client_id, COUNT(*) * 3 as total, SUM(CASE phase WHEN 'fetch' THEN 1 WHEN 'aggregate_30' THEN 2 WHEN 'done' THEN 3 ELSE 0 END) as done FROM cron_sync_status WHERE pipeline='products' GROUP BY client_id" )->fetchAll( \PDO::FETCH_ASSOC ); $data = []; foreach ( $campaigns_raw as $row ) { $data[ $row['client_id'] ]['campaigns'] = [ (int) $row['done'], (int) $row['total'] ]; } foreach ( $products_raw as $row ) { $data[ $row['client_id'] ]['products'] = [ (int) $row['done'], (int) $row['total'] ]; } // Postep pobierania URL produktow z Merchant Center - per klient (przetworzone/wszystkie). $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 ); if ( !empty( $merchant_clients_ids ) ) { $merchant_clients_ids_int = array_values( array_map( 'intval', $merchant_clients_ids ) ); $merchant_progress_rows = $mdb -> query( "SELECT p.client_id, COUNT(*) AS total, SUM( CASE WHEN ( TRIM( COALESCE( p.product_url, '' ) ) <> '' AND LOWER( TRIM( p.product_url ) ) NOT IN ( '0', '-', 'null' ) ) OR COALESCE( p.merchant_url_not_found, 0 ) = 1 THEN 1 ELSE 0 END ) AS processed FROM products p WHERE p.client_id IN (" . implode( ',', $merchant_clients_ids_int ) . ") AND TRIM( COALESCE( p.offer_id, '' ) ) <> '' GROUP BY p.client_id" ) -> fetchAll( \PDO::FETCH_ASSOC ); foreach ( $merchant_progress_rows as $row ) { $cid = (int) $row['client_id']; $total = max( 1, (int) $row['total'] ); $processed = (int) $row['processed']; $data[ $cid ]['merchant'] = [ $processed, $total ]; } // Klienci bez zadnych produktow z offer_id - oznacz jako 1/1 $seen_merchant_ids = array_column( $merchant_progress_rows, 'client_id' ); foreach ( $merchant_clients_ids_int as $cid ) { if ( !in_array( $cid, $seen_merchant_ids ) ) { $data[ $cid ]['merchant'] = [ 1, 1 ]; } } } $facebook_yesterday = date( 'Y-m-d', strtotime( '-1 day' ) ); try { $facebook_rows = $mdb -> query( "SELECT c.id AS client_id, MAX( h.date_add ) AS last_synced_date FROM clients c LEFT JOIN facebook_campaigns fc ON fc.client_id = c.id LEFT JOIN facebook_campaigns_history h ON h.facebook_campaign_id = fc.id WHERE " . $clients_not_deleted_sql_c . " AND COALESCE( c.active, 0 ) = 1 AND TRIM( COALESCE( c.facebook_ads_account_id, '' ) ) <> '' GROUP BY c.id" ) -> fetchAll( \PDO::FETCH_ASSOC ); foreach ( (array) $facebook_rows as $row ) { $facebook_client_id = (int) ( $row['client_id'] ?? 0 ); if ( $facebook_client_id <= 0 ) { continue; } $last_synced = (string) ( $row['last_synced_date'] ?? '' ); $done = ( $last_synced === $facebook_yesterday ) ? 1 : 0; $data[ $facebook_client_id ]['facebook_ads'] = [ $done, 1 ]; } $facebook_force_client_id = (int) \services\FacebookAdsApi::get_setting( 'cron_facebook_ads_force_client_id' ); if ( $facebook_force_client_id > 0 ) { $data[ $facebook_force_client_id ]['facebook_ads'] = [ 0, 1 ]; } } catch ( \Throwable $e ) { // Tabele Facebook Ads mogly nie byc jeszcze zainstalowane. } // Supplemental Feed: binarny per klient (wygenerowany dzisiaj = 1/1, nie = 0/1) $feed_today = date( 'Y-m-d' ); $feed_last_date = (string) \services\GoogleAdsApi::get_setting( 'cron_supplemental_feed_last_date' ); $feed_done_today = ( $feed_last_date === $feed_today ); if ( !empty( $merchant_clients_ids ) ) { foreach ( $merchant_clients_ids as $cid ) { $cid = (int) $cid; $data[ $cid ]['feed'] = [ $feed_done_today ? 1 : 0, 1 ]; } } echo json_encode( [ 'status' => 'ok', 'data' => $data ] ); exit; } static public function force_sync() { global $mdb; $clients_not_deleted_sql = self::sql_clients_not_deleted(); $id = (int) \S::get( 'id' ); $pipeline = trim( (string) \S::get( 'pipeline' ) ); if ( !$id ) { echo json_encode( [ 'success' => false, 'message' => 'Brak ID klienta.' ] ); exit; } $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, facebook_ads_account_id FROM clients WHERE id = :id LIMIT 1", [ ':id' => $id ] ) -> fetch( \PDO::FETCH_ASSOC ); if ( !$client || (int) ( $client['deleted'] ?? 0 ) === 1 ) { echo json_encode( [ 'success' => false, 'message' => 'Nie znaleziono klienta.' ] ); exit; } 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 if ( $pipeline === 'facebook_ads' ) { $has_facebook_id = trim( (string) ( $client['facebook_ads_account_id'] ?? '' ) ) !== ''; if ( !$has_facebook_id ) { echo json_encode( [ 'success' => false, 'message' => 'Klient nie ma ustawionego Facebook Ads Account ID.' ] ); exit; } \services\FacebookAdsApi::set_setting( 'cron_facebook_ads_force_client_id', (string) $id ); \services\FacebookAdsApi::set_setting( 'cron_facebook_ads_force_requested_at', date( 'Y-m-d H:i:s' ) ); } else if ( $pipeline === 'supplemental_feed' ) { $has_merchant_id = trim( (string) ( $client['google_merchant_account_id'] ?? '' ) ) !== ''; if ( !$has_merchant_id ) { echo json_encode( [ 'success' => false, 'message' => 'Klient nie ma ustawionego Merchant Account ID.' ] ); exit; } try { \services\SupplementalFeed::generate_for_client( $id ); echo json_encode( [ 'success' => true, 'pipeline' => 'supplemental_feed', 'immediate' => true ] ); exit; } catch ( \Throwable $e ) { echo json_encode( [ 'success' => false, 'message' => 'Blad generowania feedu: ' . $e -> getMessage() ] ); exit; } } 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; } }