feat: Implement client bestseller settings management with UI and backend support

This commit is contained in:
2026-03-04 09:20:35 +01:00
parent 8727c47315
commit 640d4c8b05
7 changed files with 684 additions and 23 deletions

View File

@@ -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": {

View File

@@ -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 3050 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 23 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

View File

@@ -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' );

View File

@@ -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;

View File

@@ -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
];
}

View File

@@ -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;

View File

@@ -48,6 +48,24 @@
</div>
</div>
<div class="products-filters" style="margin-top:10px;">
<div class="filter-group filter-group-bestseller" style="min-width: 560px;">
<label><i class="fa-solid fa-ranking-star"></i> Bestsellery reguły</label>
<div style="display:grid;grid-template-columns:repeat(4,minmax(120px,1fr));gap:8px;">
<input type="number" id="bestseller_roas_entry" class="form-control" min="0" step="0.01" placeholder="ROAS wejście" />
<input type="number" id="bestseller_roas_exit" class="form-control" min="0" step="0.01" placeholder="ROAS wyjście" />
<input type="number" id="min_conversions" class="form-control" min="0" step="1" value="10" placeholder="Min. konwersje" />
<input type="number" id="cooldown_period" class="form-control" min="1" step="1" value="14" placeholder="Cooldown (dni)" />
</div>
<div style="margin-top:8px;">
<button type="button" id="save_bestseller_rules" class="btn btn-primary btn-sm">
<i class="fa-solid fa-floppy-disk"></i> Zapisz
</button>
<span id="bestseller_rules_preview" style="margin-left:10px;color:#555;">Spelnia: -</span>
</div>
</div>
</div>
<details id="products_scope_alerts_box" class="products-scope-alerts hide">
<summary>
<i class="fa-solid fa-triangle-exclamation"></i>
@@ -121,6 +139,8 @@ var AI_CLAUDE_ENABLED = <?= $claude_enabled ? 'true' : 'false'; ?>;
var AI_GEMINI_ENABLED = <?= $gemini_enabled ? 'true' : 'false'; ?>;
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';