feat: add search and custom label filters to products view
- Added a search input for filtering products by name or ID. - Introduced a custom label input for filtering by CL4. - Implemented debounce functionality for both filters to optimize performance. - Updated local storage handling to persist filter values. - Modified styles for new filter groups in the product layout. chore: add .serena configuration files - Created .serena/.gitignore to exclude cache files. - Added .serena/project.yml for project configuration. fix: add status column to campaign_ad_groups table - Altered the campaign_ad_groups table to include a status column with ENUM values 'active' and 'paused'./c
This commit is contained in:
@@ -26,7 +26,15 @@
|
||||
"WebFetch(domain:ai.google.dev)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:oraios.github.io)",
|
||||
"Bash(which uv:*)"
|
||||
"Bash(which uv:*)",
|
||||
"mcp__serena__find_symbol",
|
||||
"mcp__serena__activate_project",
|
||||
"Bash(find:*)",
|
||||
"Bash(head:*)",
|
||||
"mcp__serena__get_symbols_overview",
|
||||
"mcp__serena__search_for_pattern",
|
||||
"mcp__serena__read_file",
|
||||
"mcp__serena__replace_content"
|
||||
]
|
||||
},
|
||||
"statusLine": {
|
||||
|
||||
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
||||
117
.serena/project.yml
Normal file
117
.serena/project.yml
Normal file
@@ -0,0 +1,117 @@
|
||||
# the name by which the project can be referenced within Serena
|
||||
project_name: "adsPRO"
|
||||
|
||||
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp
|
||||
# csharp_omnisharp dart elixir elm erlang
|
||||
# fortran fsharp go groovy haskell
|
||||
# java julia kotlin lua markdown
|
||||
# matlab nix pascal perl php
|
||||
# php_phpactor powershell python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# swift terraform toml typescript typescript_vts
|
||||
# vue yaml zig
|
||||
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# Some languages require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages:
|
||||
- typescript
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
fixed_tools: []
|
||||
|
||||
# list of mode names to that are always to be included in the set of active modes
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this setting overrides the global configuration.
|
||||
# Set this to [] to disable base modes for this project.
|
||||
# Set this to a list of mode names to always include the respective modes for this project.
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default.
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
default_modes:
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
# override of the corresponding setting in serena_config.yml, see the documentation there.
|
||||
# If null or missing, the value from the global config is used.
|
||||
symbol_info_budget:
|
||||
@@ -1577,6 +1577,7 @@ class Cron
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $ad_group_external_id,
|
||||
'ad_group_name' => $ad_group_name,
|
||||
'status' => 'paused',
|
||||
'impressions_30' => 0,
|
||||
'clicks_30' => 0,
|
||||
'cost_30' => 0,
|
||||
@@ -3477,6 +3478,8 @@ class Cron
|
||||
'ad_group_id' => (int) $ad_group_external_id
|
||||
] ] );
|
||||
|
||||
$data['status'] = 'active';
|
||||
|
||||
if ( $existing_id > 0 )
|
||||
{
|
||||
$mdb -> update( 'campaign_ad_groups', $data, [ 'id' => $existing_id ] );
|
||||
@@ -3497,21 +3500,20 @@ class Cron
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Oznacz ad_groups ktore nie pojawiaja sie juz w API jako paused (zachowaj PMax placeholder ad_group_id=0).
|
||||
if ( !empty( $campaign_db_ids ) )
|
||||
{
|
||||
$delete_where = [
|
||||
$pause_where = [
|
||||
'campaign_id' => $campaign_db_ids,
|
||||
'ad_group_id[!]' => 0
|
||||
];
|
||||
|
||||
if ( !empty( $seen_db_ids ) )
|
||||
{
|
||||
$delete_where['id[!]'] = $seen_db_ids;
|
||||
$pause_where['id[!]'] = $seen_db_ids;
|
||||
}
|
||||
|
||||
$mdb -> delete( 'campaign_ad_groups', $delete_where );
|
||||
$mdb -> update( 'campaign_ad_groups', [ 'status' => 'paused' ], $pause_where );
|
||||
}
|
||||
|
||||
return [ 'count' => $count, 'ad_group_map' => $ad_group_db_map, 'errors' => [] ];
|
||||
|
||||
@@ -830,10 +830,11 @@ class Products
|
||||
|
||||
$order_dir = $_POST['order'][0]['dir'] ? strtoupper( $_POST['order'][0]['dir'] ) : 'DESC';
|
||||
$order_name = $_POST['order'][0]['name'] ? $_POST['order'][0]['name'] : 'clicks';
|
||||
$search = $_POST['search']['value'];
|
||||
$search = trim( (string) \S::get( 'search_text' ) );
|
||||
$filter_cl4 = trim( (string) \S::get( 'filter_cl4' ) );
|
||||
|
||||
// ➊ MIN/MAX ROAS dla kontekstu klienta (opcjonalnie z filtrem search)
|
||||
$bounds = \factory\Products::get_roas_bounds( (int) $client_id, $search, $campaign_id, $ad_group_id );
|
||||
$bounds = \factory\Products::get_roas_bounds( (int) $client_id, $search, $campaign_id, $ad_group_id, $filter_cl4 );
|
||||
$roas_min = (float)$bounds['min'];
|
||||
$roas_max = (float)$bounds['max'];
|
||||
// zabezpieczenie przed dzieleniem przez 0
|
||||
@@ -868,8 +869,8 @@ class Products
|
||||
</div>';
|
||||
};
|
||||
|
||||
$db_results = \factory\Products::get_products( $client_id, $search, $limit, $start, $order_name, $order_dir, $campaign_id, $ad_group_id );
|
||||
$recordsTotal = \factory\Products::get_records_total_products( $client_id, $search, $campaign_id, $ad_group_id );
|
||||
$db_results = \factory\Products::get_products( $client_id, $search, $limit, $start, $order_name, $order_dir, $campaign_id, $ad_group_id, $filter_cl4 );
|
||||
$recordsTotal = \factory\Products::get_records_total_products( $client_id, $search, $campaign_id, $ad_group_id, $filter_cl4 );
|
||||
|
||||
$data['draw'] = \S::get( 'draw' );
|
||||
$data['recordsTotal'] = $recordsTotal;
|
||||
@@ -998,6 +999,14 @@ class Products
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function get_distinct_cl4()
|
||||
{
|
||||
$client_id = (int) \S::get( 'client_id' );
|
||||
$values = \factory\Products::get_distinct_custom_label_4( $client_id );
|
||||
echo json_encode( [ 'status' => 'ok', 'values' => $values ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function save_custom_label_4()
|
||||
{
|
||||
$product_id = \S::get( 'product_id' );
|
||||
|
||||
@@ -80,6 +80,7 @@ class Campaigns
|
||||
campaign_id,
|
||||
ad_group_id,
|
||||
ad_group_name,
|
||||
status,
|
||||
impressions_30,
|
||||
clicks_30,
|
||||
cost_30,
|
||||
@@ -94,6 +95,7 @@ class Campaigns
|
||||
roas_all_time
|
||||
FROM campaign_ad_groups
|
||||
WHERE campaign_id = :campaign_id
|
||||
AND status = \'active\'
|
||||
ORDER BY clicks_30 DESC, clicks_all_time DESC, ad_group_name ASC',
|
||||
[ ':campaign_id' => (int) $campaign_id ]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
@@ -77,16 +77,9 @@ class Products
|
||||
return false;
|
||||
}
|
||||
|
||||
$mdb -> delete( 'campaign_ad_groups', [ 'id' => $ad_group_id ] );
|
||||
$mdb -> update( 'campaign_ad_groups', [ 'status' => 'paused' ], [ '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;
|
||||
return true;
|
||||
}
|
||||
|
||||
static public function get_product_comment_by_date( $product_id, $date )
|
||||
@@ -227,9 +220,11 @@ class Products
|
||||
$sql .= ' AND pa.ad_group_id = :ad_group_id';
|
||||
$params[':ad_group_id'] = $ad_group_id;
|
||||
}
|
||||
|
||||
$sql .= ' AND ( ag.status IS NULL OR ag.status = \'active\' )';
|
||||
}
|
||||
|
||||
static public function get_products( $client_id, $search, $limit, $start, $order_name, $order_dir, $campaign_id = 0, $ad_group_id = 0 )
|
||||
static public function get_products( $client_id, $search, $limit, $start, $order_name, $order_dir, $campaign_id = 0, $ad_group_id = 0, $custom_label_4 = '' )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
@@ -310,19 +305,26 @@ class Products
|
||||
p.name LIKE :search
|
||||
OR p.title LIKE :search
|
||||
OR p.offer_id LIKE :search
|
||||
OR p.custom_label_4 LIKE :search
|
||||
OR c.campaign_name LIKE :search
|
||||
OR ag.ad_group_name LIKE :search
|
||||
)';
|
||||
$params[':search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
if ( $custom_label_4 !== '' )
|
||||
{
|
||||
$sql .= ' AND p.custom_label_4 LIKE :custom_label_4';
|
||||
$params[':custom_label_4'] = '%' . $custom_label_4 . '%';
|
||||
}
|
||||
|
||||
$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 );
|
||||
}
|
||||
|
||||
public static function get_roas_bounds( int $client_id, ?string $search = null, int $campaign_id = 0, int $ad_group_id = 0 ): array
|
||||
public static function get_roas_bounds( int $client_id, ?string $search = null, int $campaign_id = 0, int $ad_group_id = 0, string $custom_label_4 = '' ): array
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
@@ -350,12 +352,19 @@ class Products
|
||||
p.name LIKE :search
|
||||
OR p.title LIKE :search
|
||||
OR p.offer_id LIKE :search
|
||||
OR p.custom_label_4 LIKE :search
|
||||
OR c.campaign_name LIKE :search
|
||||
OR ag.ad_group_name LIKE :search
|
||||
)';
|
||||
$params[':search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
if ( $custom_label_4 !== '' )
|
||||
{
|
||||
$sql .= ' AND p.custom_label_4 LIKE :custom_label_4';
|
||||
$params[':custom_label_4'] = '%' . $custom_label_4 . '%';
|
||||
}
|
||||
|
||||
$row = $mdb -> query( $sql, $params ) -> fetch( \PDO::FETCH_ASSOC );
|
||||
|
||||
return [
|
||||
@@ -364,7 +373,7 @@ class Products
|
||||
];
|
||||
}
|
||||
|
||||
static public function get_records_total_products( $client_id, $search, $campaign_id = 0, $ad_group_id = 0 )
|
||||
static public function get_records_total_products( $client_id, $search, $campaign_id = 0, $ad_group_id = 0, $custom_label_4 = '' )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
@@ -386,12 +395,19 @@ class Products
|
||||
p.name LIKE :search
|
||||
OR p.title LIKE :search
|
||||
OR p.offer_id LIKE :search
|
||||
OR p.custom_label_4 LIKE :search
|
||||
OR c.campaign_name LIKE :search
|
||||
OR ag.ad_group_name LIKE :search
|
||||
)';
|
||||
$params[':search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
if ( $custom_label_4 !== '' )
|
||||
{
|
||||
$sql .= ' AND p.custom_label_4 LIKE :custom_label_4';
|
||||
$params[':custom_label_4'] = '%' . $custom_label_4 . '%';
|
||||
}
|
||||
|
||||
$sql .= ' GROUP BY p.id, pa.campaign_id
|
||||
) AS grouped_rows';
|
||||
|
||||
@@ -437,6 +453,30 @@ class Products
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function get_distinct_custom_label_4( $client_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$client_id = (int) $client_id;
|
||||
|
||||
if ( $client_id <= 0 )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $mdb -> query(
|
||||
"SELECT DISTINCT p.custom_label_4
|
||||
FROM products p
|
||||
WHERE p.client_id = :client_id
|
||||
AND p.custom_label_4 IS NOT NULL
|
||||
AND p.custom_label_4 != ''
|
||||
ORDER BY p.custom_label_4 ASC",
|
||||
[ ':client_id' => $client_id ]
|
||||
) -> fetchAll( \PDO::FETCH_COLUMN );
|
||||
|
||||
return $rows ?: [];
|
||||
}
|
||||
|
||||
static public function get_product_data( $product_id, $field )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1879,6 +1879,14 @@ table {
|
||||
flex: 0 0 200px;
|
||||
}
|
||||
|
||||
&.filter-group-search {
|
||||
flex: 1 1 200px;
|
||||
}
|
||||
|
||||
&.filter-group-cl4 {
|
||||
flex: 0 0 160px;
|
||||
}
|
||||
|
||||
&.filter-group-columns {
|
||||
flex: 0 0 240px;
|
||||
}
|
||||
@@ -3337,6 +3345,14 @@ table#products {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tr.product-row-unavailable {
|
||||
opacity: 0.45;
|
||||
|
||||
td {
|
||||
background-color: #f0f0f0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.products-row-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
2
migrations/024_campaign_ad_groups_status.sql
Normal file
2
migrations/024_campaign_ad_groups_status.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `campaign_ad_groups`
|
||||
ADD COLUMN `status` ENUM('active', 'paused') NOT NULL DEFAULT 'active' AFTER `ad_group_name`;
|
||||
@@ -31,6 +31,14 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group filter-group-search">
|
||||
<label for="products_search"><i class="fa-solid fa-magnifying-glass"></i> Szukaj</label>
|
||||
<input type="text" id="products_search" class="form-control" placeholder="Nazwa, ID oferty..." />
|
||||
</div>
|
||||
<div class="filter-group filter-group-cl4">
|
||||
<label for="products_cl4"><i class="fa-solid fa-tag"></i> CL4</label>
|
||||
<input type="text" id="products_cl4" class="form-control" placeholder="np. bestseller, deleted..." />
|
||||
</div>
|
||||
<div class="filter-group filter-group-columns">
|
||||
<label><i class="fa-solid fa-table-columns"></i> Kolumny</label>
|
||||
<details class="products-columns-control">
|
||||
@@ -318,6 +326,8 @@ $( function()
|
||||
d.client_id = $( '#client_id' ).val() || '';
|
||||
d.campaign_id = $( '#products_campaign_id' ).val() || '';
|
||||
d.ad_group_id = $( '#products_ad_group_id' ).val() || '';
|
||||
d.search_text = $( '#products_search' ).val() || '';
|
||||
d.filter_cl4 = $( '#products_cl4' ).val() || '';
|
||||
}
|
||||
},
|
||||
processing: true,
|
||||
@@ -352,6 +362,12 @@ $( function()
|
||||
{ width: '120px', orderable: false },
|
||||
{ width: '190px', orderable: false, className: 'dt-center' }
|
||||
],
|
||||
createdRow: function( row, data ) {
|
||||
var cl4Val = $( data[19] ).val();
|
||||
if ( cl4Val && cl4Val.toLowerCase() === 'niedostępny' ) {
|
||||
$( row ).addClass( 'product-row-unavailable' );
|
||||
}
|
||||
},
|
||||
order: [ [ 9, 'desc' ] ],
|
||||
language: {
|
||||
processing: 'Ładowanie...',
|
||||
@@ -375,6 +391,22 @@ $( function()
|
||||
products_table.ajax.reload( null, false );
|
||||
}
|
||||
|
||||
// Filtr: szukaj po nazwie/offer_id (debounce 400ms)
|
||||
var _searchTimer = null;
|
||||
$( '#products_search' ).on( 'keyup', function() {
|
||||
localStorage.setItem( 'products_search', $( this ).val() || '' );
|
||||
clearTimeout( _searchTimer );
|
||||
_searchTimer = setTimeout( function() { reload_products_table(); }, 400 );
|
||||
});
|
||||
|
||||
// Filtr: custom_label_4 (debounce 400ms)
|
||||
var _cl4Timer = null;
|
||||
$( '#products_cl4' ).on( 'keyup', function() {
|
||||
localStorage.setItem( 'products_cl4', $( this ).val() || '' );
|
||||
clearTimeout( _cl4Timer );
|
||||
_cl4Timer = setTimeout( function() { reload_products_table(); }, 400 );
|
||||
});
|
||||
|
||||
function submit_delete_campaign_ad_group( campaign_id, ad_group_id, delete_scope, on_success )
|
||||
{
|
||||
function parse_json_loose( raw )
|
||||
@@ -1068,6 +1100,10 @@ $( function()
|
||||
localStorage.setItem( 'products_client_id', client_id );
|
||||
localStorage.removeItem( 'products_campaign_id' );
|
||||
localStorage.removeItem( 'products_ad_group_id' );
|
||||
localStorage.removeItem( 'products_search' );
|
||||
localStorage.removeItem( 'products_cl4' );
|
||||
$( '#products_search' ).val( '' );
|
||||
$( '#products_cl4' ).val( '' );
|
||||
update_delete_ad_group_button_state();
|
||||
|
||||
load_products_campaigns( client_id, '' ).done( function() {
|
||||
@@ -1178,12 +1214,19 @@ $( function()
|
||||
var savedClient = localStorage.getItem( 'products_client_id' ) || '';
|
||||
var savedCampaign = localStorage.getItem( 'products_campaign_id' ) || '';
|
||||
var savedAdGroup = localStorage.getItem( 'products_ad_group_id' ) || '';
|
||||
var savedSearch = localStorage.getItem( 'products_search' ) || '';
|
||||
var savedCl4 = localStorage.getItem( 'products_cl4' ) || '';
|
||||
|
||||
if ( savedClient && $( '#client_id option[value="' + savedClient + '"]' ).length )
|
||||
{
|
||||
$( '#client_id' ).val( savedClient );
|
||||
}
|
||||
|
||||
$( '#products_search' ).val( savedSearch );
|
||||
$( '#products_cl4' ).val( savedCl4 );
|
||||
|
||||
load_cl4_suggestions( $( '#client_id' ).val() || '' );
|
||||
|
||||
load_products_campaigns( $( '#client_id' ).val() || '', savedCampaign ).done( function() {
|
||||
var selected_campaign_id = $( '#products_campaign_id' ).val() || '';
|
||||
load_products_ad_groups( selected_campaign_id, savedAdGroup ).done( function() {
|
||||
@@ -1350,6 +1393,73 @@ $( function()
|
||||
});
|
||||
});
|
||||
|
||||
// CL4 autocomplete — datalist z unikalnymi wartościami
|
||||
var cl4_values_cache = [];
|
||||
var cl4_datalist_id = 'cl4-suggestions';
|
||||
|
||||
function load_cl4_suggestions( client_id )
|
||||
{
|
||||
if ( !client_id )
|
||||
{
|
||||
cl4_values_cache = [];
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: '/products/get_distinct_cl4/client_id=' + client_id,
|
||||
type: 'GET',
|
||||
dataType: 'json'
|
||||
}).done( function( res ) {
|
||||
cl4_values_cache = ( res && res.values ) ? res.values : [];
|
||||
render_cl4_datalist();
|
||||
});
|
||||
}
|
||||
|
||||
function render_cl4_datalist()
|
||||
{
|
||||
var $dl = $( '#' + cl4_datalist_id );
|
||||
|
||||
if ( !$dl.length )
|
||||
{
|
||||
$dl = $( '<datalist id="' + cl4_datalist_id + '"></datalist>' );
|
||||
$( 'body' ).append( $dl );
|
||||
}
|
||||
|
||||
var html = '';
|
||||
|
||||
for ( var i = 0; i < cl4_values_cache.length; i++ )
|
||||
{
|
||||
html += '<option value="' + escape_html( cl4_values_cache[i] ) + '">';
|
||||
}
|
||||
|
||||
$dl.html( html );
|
||||
}
|
||||
|
||||
function bind_cl4_datalist()
|
||||
{
|
||||
$( '.custom_label_4' ).attr( 'list', cl4_datalist_id );
|
||||
}
|
||||
|
||||
// Podłącz datalist po każdym renderze tabeli
|
||||
$( '#products' ).on( 'draw.dt', function() {
|
||||
bind_cl4_datalist();
|
||||
});
|
||||
|
||||
// Załaduj sugestie po zmianie klienta
|
||||
$( 'body' ).on( 'change', '#client_id', function() {
|
||||
load_cl4_suggestions( $( this ).val() || '' );
|
||||
});
|
||||
|
||||
// Odśwież cache po zapisie CL4
|
||||
function refresh_cl4_cache_after_save()
|
||||
{
|
||||
var client_id = $( '#client_id' ).val() || '';
|
||||
if ( client_id )
|
||||
{
|
||||
load_cl4_suggestions( client_id );
|
||||
}
|
||||
}
|
||||
|
||||
// Zapis custom_label_4
|
||||
$( 'body' ).on( 'change', '.custom_label_4', function()
|
||||
{
|
||||
@@ -1376,6 +1486,7 @@ $( function()
|
||||
if ( data.status === 'ok' )
|
||||
{
|
||||
show_toast( 'Custom Label 4 zapisany.', 'success' );
|
||||
refresh_cl4_cache_after_save();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1392,12 +1503,14 @@ $( function()
|
||||
// Edycja produktu (tytuł, opis, kategoria Google)
|
||||
$( 'body' ).on( 'click', '.edit-product-title', function( e )
|
||||
{
|
||||
var current_product_name = $.trim( $( this ).closest( '.table-product-title' ).find( 'a' ).text() );
|
||||
|
||||
$.confirm({
|
||||
title: 'Edytuj produkt',
|
||||
content: '' +
|
||||
'<form action="" class="formName">' +
|
||||
'<div class="form-group">' +
|
||||
'<label>Tytuł produktu</label>' +
|
||||
'<label>Tytuł produktu <small class="text-muted" style="font-weight:normal">— ' + escape_html( current_product_name ) + '</small></label>' +
|
||||
'<div class="input-with-ai">' +
|
||||
'<input type="text" value="" product_id="' + $( this ).attr( 'product_id' ) + '" placeholder="Tytuł produktu" class="name form-control" required />' +
|
||||
( AI_OPENAI_ENABLED ? '<button type="button" class="btn btn-sm btn-ai-suggest" data-field="title" data-provider="openai" title="Zaproponuj tytuł przez ChatGPT"><i class="fa-solid fa-wand-magic-sparkles"></i> GPT</button>' : '' ) +
|
||||
|
||||
Reference in New Issue
Block a user