diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index df3c72b..ef4b3b5 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -125,8 +125,8 @@ }, "class.Products.php": { "type": "-", - "size": 47926, - "lmtime": 1772127837185, + "size": 48016, + "lmtime": 1772529696252, "modified": false }, "class.Site.php": { @@ -193,8 +193,8 @@ }, "class.Products.php": { "type": "-", - "size": 34756, - "lmtime": 1772126750737, + "size": 35004, + "lmtime": 1772529689598, "modified": false }, "class.Users.php": { @@ -243,8 +243,8 @@ }, "class.SupplementalFeed.php": { "type": "-", - "size": 3860, - "lmtime": 1772118229909, + "size": 4797, + "lmtime": 1772527954403, "modified": false } }, @@ -359,7 +359,7 @@ "supplemental_10.tsv": { "type": "-", "size": 573, - "lmtime": 1772126067385, + "lmtime": 1772528275128, "modified": false }, "supplemental_1.tsv": { @@ -370,9 +370,9 @@ }, "supplemental_2.tsv": { "type": "-", - "size": 1331, + "size": 1522, "lmtime": 1772126067543, - "modified": false + "modified": true }, "supplemental_3.tsv": { "type": "-", @@ -388,9 +388,9 @@ }, "supplemental_5.tsv": { "type": "-", - "size": 436, + "size": 1191, "lmtime": 1772126067716, - "modified": false + "modified": true }, "supplemental_6.tsv": { "type": "-", @@ -400,15 +400,15 @@ }, "supplemental_7.tsv": { "type": "-", - "size": 297, + "size": 501, "lmtime": 1772126067881, - "modified": false + "modified": true }, "supplemental_8.tsv": { "type": "-", - "size": 449, + "size": 546, "lmtime": 1772126067984, - "modified": false + "modified": true }, "supplemental_9.tsv": { "type": "-", @@ -774,8 +774,8 @@ "products": { "main_view.php": { "type": "-", - "size": 70245, - "lmtime": 1772126874952, + "size": 72463, + "lmtime": 1772530521923, "modified": false }, "product_history.php": { diff --git a/AGENTS.md b/AGENTS.md index b6bf8dd..48f25b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,21 @@ ## Sposób pracy - Pisz do mnie po polsku, zwięźle i krótko, ale merytorycznie +## Zasady pisania kodu +- Kod ma być czytelny „dla obcego”: jasne nazwy, mało magii +- Brak „skrótów na szybko” typu logika w widokach, copy-paste, losowe helpery bez spójności +- Każda funkcja/klasa ma mieć jedną odpowiedzialność, zwykle do 30–50 linii (jeśli dłuższe – dzielić) +- max 3 poziomy zagnieżdżeń (if/foreach), reszta do osobnych metod +- Nazewnictwo: + - klasy: PascalCase + - metody/zmienne: camelCase + - stałe: UPPER_SNAKE_CASE +- Zero „skrótologii” w nazwach (np. $d, $tmp, $x1) poza pętlami 2–3 linijki +- medoo + prepared statements bez wyjątków (żadnego sklejania SQL stringiem) +- XSS: escape w widokach (np. helper e()) +- CSRF dla formularzy, sensowna obsługa sesji +- Kod ma mieć komentarze tylko tam, gdzie wyjaśniają „dlaczego”, nie „co” + ## Wprowadzanie zmian - Przeanalizuj wprowadzone zadanie - Jeżeli masz jakieś wątpliwości pytaj diff --git a/autoload/controls/class.Products.php b/autoload/controls/class.Products.php index de79c64..40b8161 100644 --- a/autoload/controls/class.Products.php +++ b/autoload/controls/class.Products.php @@ -200,6 +200,78 @@ class Products exit; } + static public function get_client_bestseller_settings() + { + $client_id = (int) \S::get( 'client_id' ); + + if ( $client_id <= 0 ) + { + echo json_encode( [ + 'status' => 'error', + 'message' => 'Nieprawidlowe ID klienta.' + ] ); + exit; + } + + $settings = \factory\Products::get_client_bestseller_settings( $client_id ); + + echo json_encode( [ + 'status' => 'ok', + 'settings' => $settings + ] ); + exit; + } + + static public function save_client_bestseller_settings() + { + $client_id = (int) \S::get( 'client_id' ); + if ( $client_id <= 0 ) + { + echo json_encode( [ 'status' => 'error', 'message' => 'Nieprawidlowe ID klienta.' ] ); + exit; + } + + $settings = [ + 'bestseller_roas_entry' => \S::get( 'bestseller_roas_entry' ), + 'bestseller_roas_exit' => \S::get( 'bestseller_roas_exit' ), + 'min_conversions' => \S::get( 'min_conversions' ), + 'cooldown_period' => \S::get( 'cooldown_period' ) + ]; + + $saved = \factory\Products::save_client_bestseller_settings( $client_id, $settings ); + + echo json_encode( [ + 'status' => $saved ? 'ok' : 'error', + 'settings' => \factory\Products::get_client_bestseller_settings( $client_id ) + ] ); + exit; + } + + static public function preview_client_bestseller_settings() + { + $client_id = (int) \S::get( 'client_id' ); + if ( $client_id <= 0 ) + { + echo json_encode( [ 'status' => 'error', 'message' => 'Nieprawidlowe ID klienta.' ] ); + exit; + } + + $settings = [ + 'bestseller_roas_entry' => \S::get( 'bestseller_roas_entry' ), + 'bestseller_roas_exit' => \S::get( 'bestseller_roas_exit' ), + 'min_conversions' => \S::get( 'min_conversions' ), + 'cooldown_period' => \S::get( 'cooldown_period' ) + ]; + + $count = \factory\Products::get_client_bestseller_preview_count( $client_id, $settings ); + + echo json_encode( [ + 'status' => 'ok', + 'count' => (int) $count + ] ); + exit; + } + static public function get_scope_alerts() { $client_id = (int) \S::get( 'client_id' ); diff --git a/autoload/factory/class.Products.php b/autoload/factory/class.Products.php index 08d3f22..cdd7dad 100644 --- a/autoload/factory/class.Products.php +++ b/autoload/factory/class.Products.php @@ -204,6 +204,230 @@ class Products return $mdb -> update( 'products', [ 'min_roas' => $min_roas ], [ 'id' => $product_id ] ); } + static public function get_client_bestseller_settings( $client_id ) + { + global $mdb; + + $client_id = (int) $client_id; + if ( $client_id <= 0 ) + { + return [ + 'bestseller_roas_entry' => null, + 'bestseller_roas_exit' => null, + 'min_conversions' => 10, + 'cooldown_period' => 14 + ]; + } + + $row = $mdb -> query( + 'SELECT + bestseller_roas_entry, + bestseller_roas_exit, + min_conversions, + cooldown_period + FROM clients + WHERE id = :client_id + LIMIT 1', + [ ':client_id' => $client_id ] + ) -> fetch( \PDO::FETCH_ASSOC ); + + if ( !is_array( $row ) ) + { + return [ + 'bestseller_roas_entry' => null, + 'bestseller_roas_exit' => null, + 'min_conversions' => 10, + 'cooldown_period' => 14 + ]; + } + + $entry = isset( $row['bestseller_roas_entry'] ) ? trim( (string) $row['bestseller_roas_entry'] ) : ''; + $exit = isset( $row['bestseller_roas_exit'] ) ? trim( (string) $row['bestseller_roas_exit'] ) : ''; + $min_conversions = (int) ( $row['min_conversions'] ?? 10 ); + $cooldown_period = (int) ( $row['cooldown_period'] ?? 14 ); + + return [ + 'bestseller_roas_entry' => $entry !== '' ? (float) $entry : null, + 'bestseller_roas_exit' => $exit !== '' ? (float) $exit : null, + 'min_conversions' => $min_conversions > 0 ? $min_conversions : 10, + 'cooldown_period' => $cooldown_period > 0 ? $cooldown_period : 14 + ]; + } + + static public function save_client_bestseller_settings( $client_id, $settings ) + { + global $mdb; + + $client_id = (int) $client_id; + if ( $client_id <= 0 || !is_array( $settings ) ) + { + return false; + } + + $entry_raw = trim( (string) ( $settings['bestseller_roas_entry'] ?? '' ) ); + $exit_raw = trim( (string) ( $settings['bestseller_roas_exit'] ?? '' ) ); + + $entry = is_numeric( $entry_raw ) ? round( (float) $entry_raw, 6 ) : null; + $exit = is_numeric( $exit_raw ) ? round( (float) $exit_raw, 6 ) : null; + $min_conversions = max( 0, (int) ( $settings['min_conversions'] ?? 10 ) ); + $cooldown_period = max( 1, (int) ( $settings['cooldown_period'] ?? 14 ) ); + + return $mdb -> update( 'clients', [ + 'bestseller_roas_entry' => $entry, + 'bestseller_roas_exit' => $exit, + 'min_conversions' => $min_conversions, + 'cooldown_period' => $cooldown_period + ], [ 'id' => $client_id ] ); + } + + static public function get_client_bestseller_preview_count( $client_id, $settings ) + { + global $mdb; + + $client_id = (int) $client_id; + if ( $client_id <= 0 || !is_array( $settings ) ) + { + return 0; + } + + $entry_raw = trim( (string) ( $settings['bestseller_roas_entry'] ?? '' ) ); + $exit_raw = trim( (string) ( $settings['bestseller_roas_exit'] ?? '' ) ); + + if ( !is_numeric( $entry_raw ) || !is_numeric( $exit_raw ) ) + { + return 0; + } + + $entry = (float) $entry_raw; + $exit = (float) $exit_raw; + $min_conversions = max( 0, (float) ( $settings['min_conversions'] ?? 10 ) ); + $cooldown_period = max( 1, (int) ( $settings['cooldown_period'] ?? 14 ) ); + + $rows = $mdb -> query( + 'SELECT + p.id, + p.custom_label_4, + COALESCE( SUM( pa.cost_30 ), 0 ) AS cost_30, + COALESCE( SUM( pa.conversion_value_30 ), 0 ) AS conversion_value_30, + COALESCE( SUM( pa.conversions_30 ), 0 ) AS conversions_30 + FROM products p + LEFT JOIN products_aggregate pa ON pa.product_id = p.id + WHERE p.client_id = :client_id + GROUP BY p.id, p.custom_label_4', + [ ':client_id' => $client_id ] + ) -> fetchAll( \PDO::FETCH_ASSOC ); + + if ( empty( $rows ) ) + { + return 0; + } + + $product_ids = []; + foreach ( $rows as $row ) + { + $pid = (int) ( $row['id'] ?? 0 ); + if ( $pid > 0 ) + { + $product_ids[] = $pid; + } + } + + if ( empty( $product_ids ) ) + { + return 0; + } + + $history_by_product = []; + $history_rows = $mdb -> query( + 'SELECT + h30.product_id, + h30.date_add, + CASE + WHEN SUM( h30.cost ) > 0 THEN ( SUM( h30.conversions_value ) / SUM( h30.cost ) ) * 100 + ELSE 0 + END AS roas + FROM products_history_30 h30 + WHERE h30.product_id IN (' . implode( ',', array_map( 'intval', $product_ids ) ) . ') + GROUP BY h30.product_id, h30.date_add + ORDER BY h30.product_id ASC, h30.date_add DESC' + ) -> fetchAll( \PDO::FETCH_ASSOC ); + + foreach ( (array) $history_rows as $history_row ) + { + $pid = (int) ( $history_row['product_id'] ?? 0 ); + if ( $pid <= 0 ) + { + continue; + } + + if ( !isset( $history_by_product[ $pid ] ) ) + { + $history_by_product[ $pid ] = []; + } + + $history_by_product[ $pid ][] = (float) ( $history_row['roas'] ?? 0 ); + } + + $count = 0; + foreach ( $rows as $row ) + { + $product_id = (int) ( $row['id'] ?? 0 ); + if ( $product_id <= 0 ) + { + continue; + } + + $cost_30 = (float) ( $row['cost_30'] ?? 0 ); + $conversion_value_30 = (float) ( $row['conversion_value_30'] ?? 0 ); + $conversions_30 = (float) ( $row['conversions_30'] ?? 0 ); + $roas_30 = $cost_30 > 0 ? ( $conversion_value_30 / $cost_30 ) * 100 : 0; + $current_label = trim( (string) ( $row['custom_label_4'] ?? '' ) ); + if ( $current_label !== '' && $current_label !== 'bestseller' ) + { + continue; + } + + $entry_met = $roas_30 >= $entry && $conversions_30 >= $min_conversions; + $is_bestseller = false; + + if ( $entry_met ) + { + $is_bestseller = true; + } + else if ( $current_label === 'bestseller' ) + { + $roas_history = $history_by_product[ $product_id ] ?? []; + $has_full_window = count( $roas_history ) >= $cooldown_period; + $below_exit_all_days = true; + + if ( !$has_full_window ) + { + $below_exit_all_days = false; + } + else + { + for ( $i = 0; $i < $cooldown_period; $i++ ) + { + if ( (float) $roas_history[ $i ] >= $exit ) + { + $below_exit_all_days = false; + break; + } + } + } + + $is_bestseller = !$below_exit_all_days; + } + + if ( $is_bestseller ) + { + $count++; + } + } + + return $count; + } + static private function build_scope_filters( &$sql, &$params, $campaign_id, $ad_group_id ) { $campaign_id = (int) $campaign_id; diff --git a/autoload/services/class.SupplementalFeed.php b/autoload/services/class.SupplementalFeed.php index d28bb87..6341630 100644 --- a/autoload/services/class.SupplementalFeed.php +++ b/autoload/services/class.SupplementalFeed.php @@ -3,6 +3,154 @@ namespace services; class SupplementalFeed { + static private function get_client_bestseller_settings( $client_id ) + { + global $mdb; + + $row = $mdb -> query( + 'SELECT + bestseller_roas_entry, + bestseller_roas_exit, + min_conversions, + cooldown_period + FROM clients + WHERE id = :client_id + LIMIT 1', + [ ':client_id' => (int) $client_id ] + ) -> fetch( \PDO::FETCH_ASSOC ); + + if ( !is_array( $row ) ) + { + return [ + 'bestseller_roas_entry' => null, + 'bestseller_roas_exit' => null, + 'min_conversions' => 10, + 'cooldown_period' => 14 + ]; + } + + $entry_raw = trim( (string) ( $row['bestseller_roas_entry'] ?? '' ) ); + $exit_raw = trim( (string) ( $row['bestseller_roas_exit'] ?? '' ) ); + $min_conversions = (int) ( $row['min_conversions'] ?? 10 ); + $cooldown_period = (int) ( $row['cooldown_period'] ?? 14 ); + + return [ + 'bestseller_roas_entry' => $entry_raw !== '' ? (float) $entry_raw : null, + 'bestseller_roas_exit' => $exit_raw !== '' ? (float) $exit_raw : null, + 'min_conversions' => $min_conversions > 0 ? $min_conversions : 10, + 'cooldown_period' => $cooldown_period > 0 ? $cooldown_period : 14 + ]; + } + + static private function is_below_exit_for_cooldown( $product_id, $roas_exit, $cooldown_period ) + { + global $mdb; + + $cooldown_period = max( 1, (int) $cooldown_period ); + $rows = $mdb -> query( + 'SELECT + h30.date_add, + CASE + WHEN SUM( h30.cost ) > 0 THEN ( SUM( h30.conversions_value ) / SUM( h30.cost ) ) * 100 + ELSE 0 + END AS roas + FROM products_history_30 h30 + WHERE h30.product_id = :product_id + GROUP BY h30.date_add + ORDER BY h30.date_add DESC + LIMIT ' . $cooldown_period, + [ ':product_id' => (int) $product_id ] + ) -> fetchAll( \PDO::FETCH_ASSOC ); + + if ( count( (array) $rows ) < $cooldown_period ) + { + return false; + } + + foreach ( $rows as $row ) + { + $roas = (float) ( $row['roas'] ?? 0 ); + if ( $roas >= (float) $roas_exit ) + { + return false; + } + } + + return true; + } + + static private function refresh_bestseller_labels_for_client( $client_id ) + { + global $mdb; + + $settings = self::get_client_bestseller_settings( $client_id ); + $entry = $settings['bestseller_roas_entry']; + $exit = $settings['bestseller_roas_exit']; + $min_conversions = (float) $settings['min_conversions']; + $cooldown_period = (int) $settings['cooldown_period']; + + if ( $entry === null || $exit === null ) + { + return 0; + } + + $products = $mdb -> query( + 'SELECT + p.id, + p.custom_label_4, + COALESCE( SUM( pa.cost_30 ), 0 ) AS cost_30, + COALESCE( SUM( pa.conversion_value_30 ), 0 ) AS conversion_value_30, + COALESCE( SUM( pa.conversions_30 ), 0 ) AS conversions_30 + FROM products p + LEFT JOIN products_aggregate pa ON pa.product_id = p.id + WHERE p.client_id = :client_id + GROUP BY p.id, p.custom_label_4', + [ ':client_id' => (int) $client_id ] + ) -> fetchAll( \PDO::FETCH_ASSOC ); + + $updated_count = 0; + foreach ( (array) $products as $row ) + { + $product_id = (int) ( $row['id'] ?? 0 ); + if ( $product_id <= 0 ) + { + continue; + } + + $cost_30 = (float) ( $row['cost_30'] ?? 0 ); + $conversion_value_30 = (float) ( $row['conversion_value_30'] ?? 0 ); + $conversions_30 = (float) ( $row['conversions_30'] ?? 0 ); + $roas_30 = $cost_30 > 0 ? ( $conversion_value_30 / $cost_30 ) * 100 : 0; + + $current_label = trim( (string) ( $row['custom_label_4'] ?? '' ) ); + if ( $current_label !== '' && $current_label !== 'bestseller' ) + { + continue; + } + + $entry_met = $roas_30 >= (float) $entry && $conversions_30 >= $min_conversions; + $target_label = ''; + + if ( $entry_met ) + { + $target_label = 'bestseller'; + } + else if ( $current_label === 'bestseller' ) + { + $can_exit = self::is_below_exit_for_cooldown( $product_id, (float) $exit, $cooldown_period ); + $target_label = $can_exit ? '' : 'bestseller'; + } + + if ( $target_label !== $current_label ) + { + $mdb -> update( 'products', [ 'custom_label_4' => $target_label ], [ 'id' => $product_id ] ); + $updated_count++; + } + } + + return $updated_count; + } + /** * Generuje supplemental feed TSV dla klienta. * Zwraca tablice ze statystykami: products_total, products_written, file. @@ -12,14 +160,15 @@ class SupplementalFeed global $mdb; $client_id = (int) $client_id; + $labels_updated = self::refresh_bestseller_labels_for_client( $client_id ); $products = $mdb -> query( - "SELECT p.offer_id, p.title, p.description, p.google_product_category + "SELECT p.offer_id, p.title, p.description, p.google_product_category, p.custom_label_4 FROM products p WHERE p.client_id = :client_id AND p.offer_id IS NOT NULL AND p.offer_id <> '' - AND ( p.title IS NOT NULL OR p.description IS NOT NULL OR p.google_product_category IS NOT NULL )", + AND ( p.title IS NOT NULL OR p.description IS NOT NULL OR p.google_product_category IS NOT NULL OR p.custom_label_4 IS NOT NULL )", [ ':client_id' => $client_id ] ) -> fetchAll( \PDO::FETCH_ASSOC ); @@ -38,7 +187,7 @@ class SupplementalFeed throw new \RuntimeException( 'Nie mozna otworzyc pliku: ' . $file_path ); } - fwrite( $fp, "id\ttitle\tdescription\tgoogle_product_category\n" ); + fwrite( $fp, "id\ttitle\tdescription\tgoogle_product_category\tcustom_label_4\n" ); $written = 0; foreach ( $products as $row ) @@ -47,8 +196,9 @@ class SupplementalFeed $title = self::sanitize_for_tsv( $row['title'] ?? '' ); $description = self::sanitize_for_tsv( $row['description'] ?? '' ); $category = trim( (string) ( $row['google_product_category'] ?? '' ) ); + $custom_label_4 = trim( (string) ( $row['custom_label_4'] ?? '' ) ); - if ( $offer_id === '' || ( $title === '' && $description === '' && $category === '' ) ) + if ( $offer_id === '' || ( $title === '' && $description === '' && $category === '' && $custom_label_4 === '' ) ) { continue; } @@ -57,7 +207,8 @@ class SupplementalFeed $offer_id, $title, $description, - $category + $category, + $custom_label_4 ] ) . "\n" ); $written++; @@ -68,7 +219,8 @@ class SupplementalFeed return [ 'products_total' => count( $products ), 'products_written' => $written, - 'file' => $filename + 'file' => $filename, + 'labels_updated' => $labels_updated ]; } diff --git a/migrations/026_clients_bestseller_settings.sql b/migrations/026_clients_bestseller_settings.sql new file mode 100644 index 0000000..8b8bb36 --- /dev/null +++ b/migrations/026_clients_bestseller_settings.sql @@ -0,0 +1,62 @@ +-- Ustawienia automatycznego oznaczania bestsellerow per klient. + +SET @sql = IF( + EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'clients' + AND COLUMN_NAME = 'bestseller_roas_entry' + ), + 'DO 1', + 'ALTER TABLE `clients` ADD COLUMN `bestseller_roas_entry` DECIMAL(20,6) NULL DEFAULT NULL AFTER `google_ads_start_date`' +); +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 = 'clients' + AND COLUMN_NAME = 'bestseller_roas_exit' + ), + 'DO 1', + 'ALTER TABLE `clients` ADD COLUMN `bestseller_roas_exit` DECIMAL(20,6) NULL DEFAULT NULL AFTER `bestseller_roas_entry`' +); +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 = 'clients' + AND COLUMN_NAME = 'min_conversions' + ), + 'DO 1', + 'ALTER TABLE `clients` ADD COLUMN `min_conversions` INT(11) NOT NULL DEFAULT 10 AFTER `bestseller_roas_exit`' +); +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 = 'clients' + AND COLUMN_NAME = 'cooldown_period' + ), + 'DO 1', + 'ALTER TABLE `clients` ADD COLUMN `cooldown_period` INT(11) NOT NULL DEFAULT 14 AFTER `min_conversions`' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + diff --git a/templates/products/main_view.php b/templates/products/main_view.php index fa2c39e..6a422b6 100644 --- a/templates/products/main_view.php +++ b/templates/products/main_view.php @@ -48,6 +48,24 @@ +
+
+ +
+ + + + +
+
+ + Spelnia: - +
+
+
+
@@ -121,6 +139,8 @@ var AI_CLAUDE_ENABLED = ; var AI_GEMINI_ENABLED = ; var PRODUCTS_COLUMNS_STORAGE_KEY = 'products.columns.visibility'; var PRODUCTS_LOCKED_COLUMNS = [ 0, 21 ]; +var products_bestseller_settings_loading = false; +var products_bestseller_preview_timer = null; function show_toast( message, type ) { @@ -204,6 +224,109 @@ function products_is_locked_column( idx ) return PRODUCTS_LOCKED_COLUMNS.indexOf( Number( idx ) ) !== -1; } +function reset_client_bestseller_settings_form() +{ + $( '#bestseller_roas_entry' ).val( '' ); + $( '#bestseller_roas_exit' ).val( '' ); + $( '#min_conversions' ).val( '10' ); + $( '#cooldown_period' ).val( '14' ); + $( '#bestseller_rules_preview' ).text( 'Spelnia: -' ); +} + +function preview_client_bestseller_settings() +{ + var client_id = $( '#client_id' ).val() || ''; + + if ( !client_id || products_bestseller_settings_loading ) + { + $( '#bestseller_rules_preview' ).text( 'Spelnia: -' ); + return; + } + + $( '#bestseller_rules_preview' ).text( 'Spelnia: licze...' ); + + $.ajax({ + url: '/products/preview_client_bestseller_settings/', + type: 'POST', + dataType: 'json', + data: { + client_id: client_id, + bestseller_roas_entry: $( '#bestseller_roas_entry' ).val(), + bestseller_roas_exit: $( '#bestseller_roas_exit' ).val(), + min_conversions: $( '#min_conversions' ).val(), + cooldown_period: $( '#cooldown_period' ).val() + } + }).done( function( res ) { + if ( res && res.status === 'ok' ) + { + $( '#bestseller_rules_preview' ).text( 'Spelnia: ' + Number( res.count || 0 ) ); + } + else + { + $( '#bestseller_rules_preview' ).text( 'Spelnia: -' ); + } + }).fail( function() { + $( '#bestseller_rules_preview' ).text( 'Spelnia: -' ); + }); +} + +function load_client_bestseller_settings( client_id ) +{ + if ( !client_id ) + { + reset_client_bestseller_settings_form(); + return $.Deferred().resolve().promise(); + } + + products_bestseller_settings_loading = true; + + return $.ajax({ + url: '/products/get_client_bestseller_settings/client_id=' + client_id, + type: 'GET', + dataType: 'json' + }).done( function( res ) { + var settings = ( res && res.settings ) ? res.settings : {}; + $( '#bestseller_roas_entry' ).val( settings.bestseller_roas_entry == null ? '' : settings.bestseller_roas_entry ); + $( '#bestseller_roas_exit' ).val( settings.bestseller_roas_exit == null ? '' : settings.bestseller_roas_exit ); + $( '#min_conversions' ).val( settings.min_conversions != null ? settings.min_conversions : 10 ); + $( '#cooldown_period' ).val( settings.cooldown_period != null ? settings.cooldown_period : 14 ); + }).fail( function() { + show_toast( 'Nie udalo sie pobrac ustawien bestsellera.', 'error' ); + }).always( function() { + products_bestseller_settings_loading = false; + preview_client_bestseller_settings(); + }); +} + +function save_client_bestseller_settings() +{ + var client_id = $( '#client_id' ).val() || ''; + + if ( !client_id || products_bestseller_settings_loading ) + { + return; + } + + $.ajax({ + url: '/products/save_client_bestseller_settings/', + type: 'POST', + dataType: 'json', + data: { + client_id: client_id, + bestseller_roas_entry: $( '#bestseller_roas_entry' ).val(), + bestseller_roas_exit: $( '#bestseller_roas_exit' ).val(), + min_conversions: $( '#min_conversions' ).val(), + cooldown_period: $( '#cooldown_period' ).val() + }, + error: function() { + show_toast( 'Nie udalo sie zapisac ustawien bestsellera.', 'error' ); + }, + success: function() { + preview_client_bestseller_settings(); + } + }); +} + function products_get_saved_columns_visibility( columns_count ) { var raw = products_storage_get( PRODUCTS_COLUMNS_STORAGE_KEY ); @@ -1107,6 +1230,7 @@ $( function() $( '#products_search' ).val( '' ); $( '#products_cl4' ).val( '' ); update_delete_ad_group_button_state(); + load_client_bestseller_settings( client_id ); load_products_campaigns( client_id, '' ).done( function() { load_products_ad_groups( '', '' ).done( function() { @@ -1226,6 +1350,7 @@ $( function() $( '#products_search' ).val( savedSearch ); $( '#products_cl4' ).val( savedCl4 ); + load_client_bestseller_settings( $( '#client_id' ).val() || '' ); load_cl4_suggestions( $( '#client_id' ).val() || '' ); @@ -1435,6 +1560,17 @@ $( function() }); }); + $( 'body' ).on( 'click', '#save_bestseller_rules', function() + { + save_client_bestseller_settings(); + }); + + $( 'body' ).on( 'input change', '#bestseller_roas_entry, #bestseller_roas_exit, #min_conversions, #cooldown_period', function() + { + clearTimeout( products_bestseller_preview_timer ); + products_bestseller_preview_timer = setTimeout( preview_client_bestseller_settings, 300 ); + }); + // CL4 autocomplete — datalist z unikalnymi wartościami var cl4_values_cache = []; var cl4_datalist_id = 'cl4-suggestions';