feat: Enhance campaign terms management with keyword addition and match type change functionality
- Added a toolbar for adding keywords in the campaign terms view. - Implemented match type change functionality with a confirmation dialog. - Added delete functionality for keywords with confirmation. - Updated styles for new buttons and icons in the campaign terms view. - Enhanced product view with warning icons for product alerts and corresponding modal display. - Updated product table to include a new column for warnings and adjusted column visibility settings. - Documented project overview, code style conventions, suggested commands, and task completion checklist.
This commit is contained in:
@@ -36,7 +36,11 @@
|
||||
"mcp__serena__read_file",
|
||||
"mcp__serena__replace_content",
|
||||
"mcp__serena__replace_symbol_body",
|
||||
"mcp__serena__check_onboarding_performed"
|
||||
"mcp__serena__check_onboarding_performed",
|
||||
"mcp__serena__find_file",
|
||||
"mcp__serena__onboarding",
|
||||
"mcp__serena__write_memory",
|
||||
"mcp__serena__list_dir"
|
||||
]
|
||||
},
|
||||
"statusLine": {
|
||||
|
||||
26
.serena/memories/project_overview.md
Normal file
26
.serena/memories/project_overview.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# adsPRO - Project Overview
|
||||
|
||||
## Purpose
|
||||
PHP SaaS application for managing Google Ads campaigns, products, and clients. Integrates with Google Ads API, OpenAI, and Claude AI for AI-powered ad optimization. UI language: Polish.
|
||||
|
||||
## Tech Stack
|
||||
- PHP (custom MVC framework, no Composer)
|
||||
- MySQL via Medoo ORM (`global $mdb`)
|
||||
- Frontend: jQuery 3.6, DataTables 2.1, Bootstrap 4, Select2, Highcharts, Font Awesome 6.5
|
||||
- SASS for styles (compiled by VS Code Live Sass Compiler)
|
||||
- Deployment: FTP auto-upload via VS Code FTP-Kr
|
||||
|
||||
## Architecture
|
||||
- Controllers: `autoload/controls/class.*.php` (namespace `\controls`)
|
||||
- Factories: `autoload/factory/class.*.php` (namespace `\factory`)
|
||||
- Views: `autoload/view/class.*.php` (namespace `\view`)
|
||||
- Services: `autoload/services/class.*.php` (namespace `\services`)
|
||||
- Templates: `templates/` (variables via `$this->varName`)
|
||||
- Routing: `index.php`, URL `/module/action/` → `\controls\Module::action()`
|
||||
|
||||
## Entry Points
|
||||
- `index.php` - Main app (routing + auth)
|
||||
- `ajax.php` - AJAX requests (authenticated)
|
||||
- `api.php` - Public API
|
||||
- `cron.php` - Background jobs
|
||||
- `install.php` - Database migration runner
|
||||
9
.serena/memories/style_conventions.md
Normal file
9
.serena/memories/style_conventions.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Code Style & Conventions
|
||||
|
||||
- Spaces inside parentheses: `if ( $x )`, `function( $a, $b )`
|
||||
- Braces on new line for classes/functions
|
||||
- 4-space indent in classes, 2-space in templates
|
||||
- All controller/factory methods: `static public function`
|
||||
- JSON endpoints: `echo json_encode([...]); exit;`
|
||||
- Classes PascalCase, methods/variables/columns snake_case
|
||||
- Commit messages in Polish, prefixed with `feat:`, `fix:`, etc.
|
||||
19
.serena/memories/suggested_commands.md
Normal file
19
.serena/memories/suggested_commands.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Suggested Commands
|
||||
|
||||
## Database Migrations
|
||||
```bash
|
||||
php install.php # Run migrations
|
||||
php install.php --force # Force re-run all
|
||||
```
|
||||
|
||||
## System Utils (Windows/Git Bash)
|
||||
- `git` - version control
|
||||
- `ls` - list files (Git Bash)
|
||||
- `grep` - search (Git Bash)
|
||||
- `find` - find files (Git Bash)
|
||||
|
||||
## No build/test/lint commands
|
||||
- No Composer, no package.json
|
||||
- SASS compiled by VS Code Live Sass Compiler
|
||||
- Deployment via FTP-Kr extension (auto-upload)
|
||||
- No automated test suite
|
||||
7
.serena/memories/task_completion.md
Normal file
7
.serena/memories/task_completion.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Task Completion Checklist
|
||||
|
||||
1. Verify PHP syntax (no parse errors)
|
||||
2. Test in browser if possible
|
||||
3. Check SQL migrations if DB changes needed (add to `migrations/`)
|
||||
4. Files auto-deploy via FTP-Kr on save
|
||||
5. No automated tests to run
|
||||
@@ -27,6 +27,7 @@ project_name: "adsPRO"
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages:
|
||||
- typescript
|
||||
- php
|
||||
|
||||
# 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
|
||||
|
||||
3
.vscode/ftp-kr.json
vendored
3
.vscode/ftp-kr.json
vendored
@@ -16,6 +16,7 @@
|
||||
"/.serena",
|
||||
"/.claude",
|
||||
"CLAUDE.md",
|
||||
"AGENTS.md"
|
||||
"AGENTS.md",
|
||||
".gitignore"
|
||||
]
|
||||
}
|
||||
127
.vscode/ftp-kr.sync.cache.json
vendored
127
.vscode/ftp-kr.sync.cache.json
vendored
@@ -101,8 +101,8 @@
|
||||
},
|
||||
"class.Cron.php": {
|
||||
"type": "-",
|
||||
"size": 182399,
|
||||
"lmtime": 1771755214561,
|
||||
"size": 182635,
|
||||
"lmtime": 1771851157171,
|
||||
"modified": false
|
||||
},
|
||||
"class.FacebookAds.php": {
|
||||
@@ -201,7 +201,7 @@
|
||||
"services": {
|
||||
"class.ClaudeApi.php": {
|
||||
"type": "-",
|
||||
"size": 12510,
|
||||
"size": 12408,
|
||||
"lmtime": 1771198088093,
|
||||
"modified": true
|
||||
},
|
||||
@@ -211,15 +211,21 @@
|
||||
"lmtime": 1771619153702,
|
||||
"modified": false
|
||||
},
|
||||
"class.GeminiApi.php": {
|
||||
"type": "-",
|
||||
"size": 13361,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"class.GoogleAdsApi.php": {
|
||||
"type": "-",
|
||||
"size": 114140,
|
||||
"lmtime": 1771444236566,
|
||||
"modified": true
|
||||
"size": 116731,
|
||||
"lmtime": 1771851152230,
|
||||
"modified": false
|
||||
},
|
||||
"class.OpenAiApi.php": {
|
||||
"type": "-",
|
||||
"size": 18739,
|
||||
"size": 29408,
|
||||
"lmtime": 1771171891986,
|
||||
"modified": true
|
||||
}
|
||||
@@ -251,20 +257,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
".claude": {
|
||||
"settings.local.json": {
|
||||
"type": "-",
|
||||
"size": 1421,
|
||||
"lmtime": 1771755894778,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"CLAUDE.md": {
|
||||
"type": "-",
|
||||
"size": 4139,
|
||||
"lmtime": 1771368527045,
|
||||
"modified": false
|
||||
},
|
||||
"config.php": {
|
||||
"type": "-",
|
||||
"size": 921,
|
||||
@@ -606,24 +598,6 @@
|
||||
"lmtime": 1744488227849,
|
||||
"modified": false
|
||||
},
|
||||
".serena": {
|
||||
"cache": {
|
||||
"typescript": {
|
||||
"raw_document_symbols.pkl": {
|
||||
"type": "-",
|
||||
"size": 23480,
|
||||
"lmtime": 1771755819379,
|
||||
"modified": false
|
||||
},
|
||||
"document_symbols.pkl": {
|
||||
"type": "-",
|
||||
"size": 142667,
|
||||
"lmtime": 1771755819382,
|
||||
"modified": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
"products": {
|
||||
"main_view.php": {
|
||||
@@ -714,7 +688,80 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tmp": {},
|
||||
"tmp": {
|
||||
"campaign_alerts_debug.log": {
|
||||
"type": "-",
|
||||
"size": 15260,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"__check_adgroups.php": {
|
||||
"type": "-",
|
||||
"size": 1579,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"check_db.php": {
|
||||
"type": "-",
|
||||
"size": 1274,
|
||||
"lmtime": 1771850655635,
|
||||
"modified": false
|
||||
},
|
||||
"check_db_remote.php": {
|
||||
"type": "-",
|
||||
"size": 450,
|
||||
"lmtime": 1771850670102,
|
||||
"modified": false
|
||||
},
|
||||
"check_patterns.php": {
|
||||
"type": "-",
|
||||
"size": 2250,
|
||||
"lmtime": 1771850693804,
|
||||
"modified": false
|
||||
},
|
||||
"check_schema.php": {
|
||||
"type": "-",
|
||||
"size": 530,
|
||||
"lmtime": 1771850676855,
|
||||
"modified": false
|
||||
},
|
||||
"debug_clients.php": {
|
||||
"type": "-",
|
||||
"size": 1274,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"debug_clients_remote2.php": {
|
||||
"type": "-",
|
||||
"size": 1106,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"debug_clients_remote.php": {
|
||||
"type": "-",
|
||||
"size": 1344,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"debug_eligible_remote.php": {
|
||||
"type": "-",
|
||||
"size": 567,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"meta_active_last30d.json": {
|
||||
"type": "-",
|
||||
"size": 168510,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"check_active_date_logs.php": {
|
||||
"type": "-",
|
||||
"size": 928,
|
||||
"lmtime": 1771850708365,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"tools": {},
|
||||
"upload": {},
|
||||
"xml": {}
|
||||
|
||||
@@ -669,4 +669,277 @@ class CampaignTerms
|
||||
echo json_encode( $response );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function update_keyword_match_type()
|
||||
{
|
||||
$keyword_id = (int) \S::get( 'keyword_id' );
|
||||
$new_match_type = strtoupper( trim( (string) \S::get( 'new_match_type' ) ) );
|
||||
|
||||
if ( $keyword_id <= 0 )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Nie podano frazy.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( !in_array( $new_match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Nieprawidlowy typ dopasowania.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$context = \factory\Campaigns::get_keyword_context( $keyword_id );
|
||||
if ( !$context )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Nie znaleziono danych frazy.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$old_match_type = strtoupper( trim( (string) ( $context['match_type'] ?? '' ) ) );
|
||||
if ( $old_match_type === $new_match_type )
|
||||
{
|
||||
echo json_encode( [ 'success' => true, 'message' => 'Dopasowanie jest juz ustawione na ' . $new_match_type . '.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$customer_id = trim( (string) ( $context['google_ads_customer_id'] ?? '' ) );
|
||||
$ad_group_external_id = trim( (string) ( $context['external_ad_group_id'] ?? '' ) );
|
||||
$keyword_text = trim( (string) ( $context['keyword_text'] ?? '' ) );
|
||||
|
||||
if ( $customer_id === '' || $ad_group_external_id === '' || $keyword_text === '' )
|
||||
{
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Brak wymaganych danych Google Ads dla tej frazy.',
|
||||
], [ 'context' => $context ] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
$api = new \services\GoogleAdsApi();
|
||||
if ( !$api -> is_configured() )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Google Ads API nie jest skonfigurowane.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$api_result = $api -> update_keyword_match_type( $customer_id, $ad_group_external_id, $keyword_text, $old_match_type, $new_match_type );
|
||||
|
||||
if ( !( $api_result['success'] ?? false ) )
|
||||
{
|
||||
$last_error = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Nie udalo sie zmienic dopasowania frazy w Google Ads.',
|
||||
'error' => $last_error
|
||||
], [
|
||||
'customer_id' => $customer_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'old_match_type' => $old_match_type,
|
||||
'new_match_type' => $new_match_type,
|
||||
'api_result' => $api_result
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
\factory\Campaigns::update_keyword_match_type( $keyword_id, $new_match_type );
|
||||
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => true,
|
||||
'message' => 'Dopasowanie zmienione z ' . $old_match_type . ' na ' . $new_match_type . '.'
|
||||
], [
|
||||
'customer_id' => $customer_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'old_match_type' => $old_match_type,
|
||||
'new_match_type' => $new_match_type,
|
||||
'api_result' => $api_result
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function add_keyword()
|
||||
{
|
||||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||||
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||||
$keyword_text = trim( (string) \S::get( 'keyword_text' ) );
|
||||
$match_type = strtoupper( trim( (string) \S::get( 'match_type' ) ) );
|
||||
|
||||
if ( $campaign_id <= 0 || $ad_group_id <= 0 )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Wybierz kampanie i grupe reklam.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( $keyword_text === '' )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Wpisz fraze do dodania.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
|
||||
{
|
||||
$match_type = 'BROAD';
|
||||
}
|
||||
|
||||
global $mdb;
|
||||
|
||||
$campaign_data = $mdb -> query(
|
||||
'SELECT c.campaign_id AS external_campaign_id, cl.google_ads_customer_id
|
||||
FROM campaigns AS c
|
||||
INNER JOIN clients AS cl ON cl.id = c.client_id
|
||||
WHERE c.id = :campaign_id
|
||||
LIMIT 1',
|
||||
[ ':campaign_id' => $campaign_id ]
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
|
||||
if ( !$campaign_data )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Nie znaleziono kampanii.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$ad_group_data = $mdb -> query(
|
||||
'SELECT ad_group_id AS external_ad_group_id FROM campaign_ad_groups WHERE id = :ad_group_id LIMIT 1',
|
||||
[ ':ad_group_id' => $ad_group_id ]
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
|
||||
if ( !$ad_group_data )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Nie znaleziono grupy reklam.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$customer_id = trim( (string) ( $campaign_data['google_ads_customer_id'] ?? '' ) );
|
||||
$ad_group_external_id = trim( (string) ( $ad_group_data['external_ad_group_id'] ?? '' ) );
|
||||
|
||||
if ( $customer_id === '' || $ad_group_external_id === '' )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Brak wymaganych danych Google Ads.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$api = new \services\GoogleAdsApi();
|
||||
if ( !$api -> is_configured() )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Google Ads API nie jest skonfigurowane.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$api_result = $api -> add_keyword_to_ad_group( $customer_id, $ad_group_external_id, $keyword_text, $match_type );
|
||||
|
||||
if ( !( $api_result['success'] ?? false ) )
|
||||
{
|
||||
$last_error = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Nie udalo sie dodac frazy w Google Ads.',
|
||||
'error' => $last_error
|
||||
], [
|
||||
'customer_id' => $customer_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'match_type' => $match_type,
|
||||
'api_result' => $api_result
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
\factory\Campaigns::insert_campaign_keyword( $campaign_id, $ad_group_id, $keyword_text, $match_type );
|
||||
|
||||
$match_labels = [ 'PHRASE' => 'do wyrazenia', 'EXACT' => 'scisle', 'BROAD' => 'przyblizone' ];
|
||||
$match_label = $match_labels[$match_type] ?? $match_type;
|
||||
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => true,
|
||||
'message' => ( $api_result['duplicate'] ?? false )
|
||||
? 'Fraza juz istnieje w Google Ads (dopasowanie ' . $match_label . ').'
|
||||
: 'Fraza zostala dodana (dopasowanie ' . $match_label . ').',
|
||||
'duplicate' => (bool) ( $api_result['duplicate'] ?? false )
|
||||
], [
|
||||
'customer_id' => $customer_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'match_type' => $match_type,
|
||||
'api_result' => $api_result
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function delete_keyword()
|
||||
{
|
||||
$keyword_id = (int) \S::get( 'keyword_id' );
|
||||
|
||||
if ( $keyword_id <= 0 )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Nie podano frazy do usuniecia.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$context = \factory\Campaigns::get_keyword_context( $keyword_id );
|
||||
if ( !$context )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Nie znaleziono danych frazy.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$customer_id = trim( (string) ( $context['google_ads_customer_id'] ?? '' ) );
|
||||
$ad_group_external_id = trim( (string) ( $context['external_ad_group_id'] ?? '' ) );
|
||||
$keyword_text = trim( (string) ( $context['keyword_text'] ?? '' ) );
|
||||
$match_type = strtoupper( trim( (string) ( $context['match_type'] ?? 'BROAD' ) ) );
|
||||
|
||||
if ( $customer_id === '' || $ad_group_external_id === '' || $keyword_text === '' )
|
||||
{
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Brak wymaganych danych Google Ads dla tej frazy.',
|
||||
], [ 'context' => $context ] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
$api = new \services\GoogleAdsApi();
|
||||
if ( !$api -> is_configured() )
|
||||
{
|
||||
echo json_encode( [ 'success' => false, 'message' => 'Google Ads API nie jest skonfigurowane.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
$api_result = $api -> remove_keyword_from_ad_group( $customer_id, $ad_group_external_id, $keyword_text, $match_type );
|
||||
|
||||
if ( !( $api_result['success'] ?? false ) )
|
||||
{
|
||||
$last_error = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => false,
|
||||
'message' => 'Nie udalo sie usunac frazy w Google Ads.',
|
||||
'error' => $last_error
|
||||
], [
|
||||
'customer_id' => $customer_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'match_type' => $match_type,
|
||||
'api_result' => $api_result
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
\factory\Campaigns::delete_campaign_keyword( $keyword_id );
|
||||
|
||||
$removed = (int) ( $api_result['removed'] ?? 0 );
|
||||
$message = $removed > 0
|
||||
? 'Fraza zostala usunieta z Google Ads.'
|
||||
: 'Fraza nie byla juz obecna w Google Ads. Usunieto lokalny wpis.';
|
||||
|
||||
echo json_encode( self::with_optional_debug( [
|
||||
'success' => true,
|
||||
'message' => $message,
|
||||
'removed' => $removed
|
||||
], [
|
||||
'customer_id' => $customer_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'match_type' => $match_type,
|
||||
'api_result' => $api_result
|
||||
] ) );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -872,6 +872,10 @@ class Products
|
||||
$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 );
|
||||
|
||||
// Sredni CR konta — do obliczenia progu klikniec
|
||||
$account_cr = \factory\Products::get_account_conversion_rate( (int) $client_id );
|
||||
if ( $account_cr <= 0 ) $account_cr = 0.02; // fallback 2%
|
||||
|
||||
$data['draw'] = \S::get( 'draw' );
|
||||
$data['recordsTotal'] = $recordsTotal;
|
||||
$data['recordsFiltered'] = $recordsTotal;
|
||||
@@ -920,6 +924,45 @@ class Products
|
||||
? '<a href="' . htmlspecialchars( $product_url ) . '" target="_blank" rel="noopener noreferrer" title="' . htmlspecialchars( $product_url ) . '"><i class="fa-solid fa-up-right-from-square"></i> Otworz</a>'
|
||||
: '';
|
||||
|
||||
// Algorytm ostrzezen produktowych
|
||||
$warnings = [];
|
||||
$clicks = (int) $row['clicks'];
|
||||
$conversions = (float) $row['conversions'];
|
||||
$ctr = (float) $row['ctr'];
|
||||
$min_roas_val = (float) $row['min_roas'];
|
||||
$click_threshold = (int) ceil( 3 * ( 1 / $account_cr ) );
|
||||
|
||||
// 1. Niski CTR po 30+ klikniec
|
||||
if ( $clicks >= 30 && $ctr < 0.5 )
|
||||
{
|
||||
$warnings[] = 'Niski CTR (' . round( $ctr, 2 ) . '%) po ' . $clicks . ' kliknieciach — prawdopodobny problem z tytulem, zdjeciem lub cena.';
|
||||
}
|
||||
|
||||
// 2. Zero konwersji po progu klikniec
|
||||
if ( $clicks >= $click_threshold && $conversions == 0 )
|
||||
{
|
||||
$warnings[] = 'Brak konwersji po ' . $clicks . ' kliknieciach (prog: ' . $click_threshold . '). Produkt prawdopodobnie nie sprzedaje sie z reklam.';
|
||||
}
|
||||
|
||||
// 3. ROAS ponizej progu rentownosci
|
||||
if ( $clicks >= $click_threshold && $conversions > 0 && $min_roas_val > 0 && $roasValue < $min_roas_val )
|
||||
{
|
||||
$warnings[] = 'ROAS ' . $roasDisplay . '% ponizej progu rentownosci (' . (int) $min_roas_val . '%). Rozważ optymalizacje lub wylaczenie.';
|
||||
}
|
||||
|
||||
// 4. Strefa zolta — ROAS bliski progu (80-100% min_roas)
|
||||
if ( $clicks >= $click_threshold && $conversions > 0 && $min_roas_val > 0 && $roasValue >= $min_roas_val && $roasValue < $min_roas_val * 1.5 )
|
||||
{
|
||||
$warnings[] = 'ROAS ' . $roasDisplay . '% — blisko progu rentownosci (' . (int) $min_roas_val . '%). Daj jeszcze 50-100 klikniec lub zoptymalizuj listing.';
|
||||
}
|
||||
|
||||
$warningHtml = '';
|
||||
if ( !empty( $warnings ) )
|
||||
{
|
||||
$warningTitle = htmlspecialchars( implode( "\n", $warnings ), ENT_QUOTES );
|
||||
$warningHtml = '<span class="product-warning-icon" data-warnings="' . $warningTitle . '" title="Produkt ma problemy"><i class="fa-solid fa-triangle-exclamation"></i></span>';
|
||||
}
|
||||
|
||||
$data['data'][] = [
|
||||
'', // checkbox column
|
||||
$row['product_id'],
|
||||
@@ -935,9 +978,10 @@ class Products
|
||||
<i class="fa fa-pencil"></i>
|
||||
</span>
|
||||
</div>',
|
||||
$warningHtml,
|
||||
$row['impressions'],
|
||||
$row['impressions_30'],
|
||||
'<span style="color: ' . ( $row['clicks'] > 200 ? ( $row['clicks'] > 400 ? '#0047ccff' : '#57b951' ) : '' ) . '">' . $row['clicks'] . '</span>',
|
||||
$row['clicks'],
|
||||
$row['clicks_30'],
|
||||
round( $row['ctr'], 2 ) . '%',
|
||||
\S::number_display( $row['cost'] ),
|
||||
|
||||
@@ -319,6 +319,60 @@ class Campaigns
|
||||
return $mdb -> delete( 'campaign_negative_keywords', [ 'id' => (int) $negative_keyword_row_id ] );
|
||||
}
|
||||
|
||||
static public function get_keyword_context( $keyword_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
return $mdb -> query(
|
||||
'SELECT
|
||||
kw.id AS keyword_row_id,
|
||||
kw.keyword_text,
|
||||
kw.match_type,
|
||||
kw.campaign_id AS db_campaign_id,
|
||||
kw.ad_group_id AS db_ad_group_id,
|
||||
c.client_id,
|
||||
c.campaign_id AS external_campaign_id,
|
||||
ag.ad_group_id AS external_ad_group_id,
|
||||
cl.google_ads_customer_id
|
||||
FROM campaign_keywords AS kw
|
||||
INNER JOIN campaigns AS c ON c.id = kw.campaign_id
|
||||
INNER JOIN clients AS cl ON cl.id = c.client_id
|
||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = kw.ad_group_id
|
||||
WHERE kw.id = :keyword_id
|
||||
LIMIT 1',
|
||||
[ ':keyword_id' => (int) $keyword_id ]
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function update_keyword_match_type( $keyword_id, $new_match_type )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> update( 'campaign_keywords', [
|
||||
'match_type' => strtoupper( trim( (string) $new_match_type ) )
|
||||
], [ 'id' => (int) $keyword_id ] );
|
||||
}
|
||||
|
||||
static public function insert_campaign_keyword( $campaign_id, $ad_group_id, $keyword_text, $match_type )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$mdb -> insert( 'campaign_keywords', [
|
||||
'campaign_id' => (int) $campaign_id,
|
||||
'ad_group_id' => (int) $ad_group_id,
|
||||
'keyword_text' => trim( (string) $keyword_text ),
|
||||
'match_type' => strtoupper( trim( (string) $match_type ) ),
|
||||
'date_sync' => date( 'Y-m-d' )
|
||||
] );
|
||||
|
||||
return (int) $mdb -> id();
|
||||
}
|
||||
|
||||
static public function delete_campaign_keyword( $keyword_id )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> delete( 'campaign_keywords', [ 'id' => (int) $keyword_id ] );
|
||||
}
|
||||
|
||||
static public function delete_campaign( $campaign_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
@@ -373,6 +373,28 @@ class Products
|
||||
];
|
||||
}
|
||||
|
||||
static public function get_account_conversion_rate( $client_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$row = $mdb -> query(
|
||||
'SELECT SUM( pa.clicks_all_time ) AS total_clicks,
|
||||
SUM( pa.conversions_all_time ) AS total_conversions
|
||||
FROM products_aggregate AS pa
|
||||
INNER JOIN products AS p ON p.id = pa.product_id
|
||||
WHERE p.client_id = :client_id
|
||||
AND pa.clicks_all_time > 0',
|
||||
[ ':client_id' => (int) $client_id ]
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
|
||||
$total_clicks = (float) ( $row['total_clicks'] ?? 0 );
|
||||
$total_conversions = (float) ( $row['total_conversions'] ?? 0 );
|
||||
|
||||
if ( $total_clicks <= 0 ) return 0;
|
||||
|
||||
return $total_conversions / $total_clicks;
|
||||
}
|
||||
|
||||
static public function get_records_total_products( $client_id, $search, $campaign_id = 0, $ad_group_id = 0, $custom_label_4 = '' )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
@@ -1987,6 +1987,162 @@ class GoogleAdsApi
|
||||
return str_replace( [ '\\', '\'' ], [ '\\\\', '\\\'' ], (string) $value );
|
||||
}
|
||||
|
||||
public function add_keyword_to_ad_group( $customer_id, $ad_group_id, $keyword_text, $match_type = 'BROAD' )
|
||||
{
|
||||
$customer_id = trim( str_replace( '-', '', (string) $customer_id ) );
|
||||
$ad_group_id = trim( (string) $ad_group_id );
|
||||
$keyword_text = trim( (string) $keyword_text );
|
||||
$match_type = strtoupper( trim( (string) $match_type ) );
|
||||
|
||||
if ( $customer_id === '' || $ad_group_id === '' || $keyword_text === '' )
|
||||
{
|
||||
self::set_setting( 'google_ads_last_error', 'Brak wymaganych danych do dodania frazy.' );
|
||||
return [ 'success' => false, 'duplicate' => false ];
|
||||
}
|
||||
|
||||
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
|
||||
{
|
||||
$match_type = 'BROAD';
|
||||
}
|
||||
|
||||
$operation = [
|
||||
'adGroupCriterionOperation' => [
|
||||
'create' => [
|
||||
'adGroup' => 'customers/' . $customer_id . '/adGroups/' . $ad_group_id,
|
||||
'keyword' => [
|
||||
'text' => $keyword_text,
|
||||
'matchType' => $match_type
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$result = $this -> mutate( $customer_id, [ $operation ] );
|
||||
|
||||
if ( $result === false )
|
||||
{
|
||||
$last_error = (string) self::get_setting( 'google_ads_last_error' );
|
||||
$is_duplicate = stripos( $last_error, 'DUPLICATE' ) !== false
|
||||
|| stripos( $last_error, 'already exists' ) !== false;
|
||||
|
||||
if ( $is_duplicate )
|
||||
{
|
||||
return [ 'success' => true, 'duplicate' => true ];
|
||||
}
|
||||
|
||||
return [ 'success' => false, 'duplicate' => false, 'sent_operation' => $operation ];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'duplicate' => false,
|
||||
'response' => $result,
|
||||
'sent_operation' => $operation
|
||||
];
|
||||
}
|
||||
|
||||
public function remove_keyword_from_ad_group( $customer_id, $ad_group_id, $keyword_text, $match_type = 'BROAD' )
|
||||
{
|
||||
$customer_id = trim( str_replace( '-', '', (string) $customer_id ) );
|
||||
$ad_group_id = trim( (string) $ad_group_id );
|
||||
$keyword_text = trim( (string) $keyword_text );
|
||||
$match_type = strtoupper( trim( (string) $match_type ) );
|
||||
|
||||
if ( $customer_id === '' || $ad_group_id === '' || $keyword_text === '' )
|
||||
{
|
||||
self::set_setting( 'google_ads_last_error', 'Brak wymaganych danych do usuniecia frazy.' );
|
||||
return [ 'success' => false, 'removed' => 0 ];
|
||||
}
|
||||
|
||||
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
|
||||
{
|
||||
$match_type = 'BROAD';
|
||||
}
|
||||
|
||||
$keyword_text_escaped = $this -> gaql_escape( $keyword_text );
|
||||
$gaql = "SELECT "
|
||||
. "ad_group_criterion.resource_name "
|
||||
. "FROM ad_group_criterion "
|
||||
. "WHERE ad_group.id = " . $ad_group_id . " "
|
||||
. "AND ad_group_criterion.type = 'KEYWORD' "
|
||||
. "AND ad_group_criterion.negative = FALSE "
|
||||
. "AND ad_group_criterion.keyword.text = '" . $keyword_text_escaped . "' "
|
||||
. "AND ad_group_criterion.keyword.match_type = " . $match_type . " "
|
||||
. "LIMIT 50";
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql );
|
||||
if ( $results === false )
|
||||
{
|
||||
return [ 'success' => false, 'removed' => 0 ];
|
||||
}
|
||||
|
||||
$resource_names = [];
|
||||
foreach ( (array) $results as $row )
|
||||
{
|
||||
$resource_name = (string) ( $row['adGroupCriterion']['resourceName'] ?? '' );
|
||||
if ( $resource_name === '' )
|
||||
{
|
||||
$resource_name = (string) ( $row['ad_group_criterion']['resource_name'] ?? '' );
|
||||
}
|
||||
if ( $resource_name !== '' )
|
||||
{
|
||||
$resource_names[] = $resource_name;
|
||||
}
|
||||
}
|
||||
$resource_names = array_values( array_unique( $resource_names ) );
|
||||
|
||||
if ( empty( $resource_names ) )
|
||||
{
|
||||
return [ 'success' => true, 'removed' => 0, 'not_found' => true ];
|
||||
}
|
||||
|
||||
$operations = [];
|
||||
foreach ( $resource_names as $resource_name )
|
||||
{
|
||||
$operations[] = [
|
||||
'adGroupCriterionOperation' => [
|
||||
'remove' => $resource_name
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$mutate_result = $this -> mutate( $customer_id, $operations );
|
||||
if ( $mutate_result === false )
|
||||
{
|
||||
return [ 'success' => false, 'removed' => 0, 'sent_operations' => $operations ];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'removed' => count( $resource_names ),
|
||||
'response' => $mutate_result,
|
||||
'sent_operations' => $operations
|
||||
];
|
||||
}
|
||||
|
||||
public function update_keyword_match_type( $customer_id, $ad_group_id, $keyword_text, $old_match_type, $new_match_type )
|
||||
{
|
||||
$remove_result = $this -> remove_keyword_from_ad_group( $customer_id, $ad_group_id, $keyword_text, $old_match_type );
|
||||
|
||||
if ( !( $remove_result['success'] ?? false ) && !( $remove_result['not_found'] ?? false ) )
|
||||
{
|
||||
return [ 'success' => false, 'step' => 'remove', 'remove_result' => $remove_result ];
|
||||
}
|
||||
|
||||
$add_result = $this -> add_keyword_to_ad_group( $customer_id, $ad_group_id, $keyword_text, $new_match_type );
|
||||
|
||||
if ( !( $add_result['success'] ?? false ) )
|
||||
{
|
||||
return [ 'success' => false, 'step' => 'add', 'add_result' => $add_result, 'remove_result' => $remove_result ];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'remove_result' => $remove_result,
|
||||
'add_result' => $add_result
|
||||
];
|
||||
}
|
||||
|
||||
private function verify_negative_keyword_exists( $customer_id, $scope, $keyword_text, $match_type, $campaign_id = null, $ad_group_id = null )
|
||||
{
|
||||
$customer_id = trim( str_replace( '-', '', (string) $customer_id ) );
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2816,7 +2816,8 @@ table#products {
|
||||
}
|
||||
}
|
||||
|
||||
.terms-negative-toolbar {
|
||||
.terms-negative-toolbar,
|
||||
.terms-keywords-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
@@ -3136,7 +3137,8 @@ table#products {
|
||||
}
|
||||
|
||||
.terms-add-negative-btn,
|
||||
.terms-remove-negative-btn {
|
||||
.terms-remove-negative-btn,
|
||||
.terms-delete-keyword-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -3159,7 +3161,8 @@ table#products {
|
||||
}
|
||||
}
|
||||
|
||||
.terms-remove-negative-btn {
|
||||
.terms-remove-negative-btn,
|
||||
.terms-delete-keyword-btn {
|
||||
border: 1px solid #FECACA;
|
||||
background: #FEF2F2;
|
||||
color: #DC2626;
|
||||
@@ -3171,6 +3174,26 @@ table#products {
|
||||
}
|
||||
}
|
||||
|
||||
.terms-change-match-type-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid #E2E8F0;
|
||||
background: #EEF2FF;
|
||||
color: #3B82F6;
|
||||
|
||||
&:hover {
|
||||
background: #3B82F6;
|
||||
color: #FFFFFF;
|
||||
border-color: #3B82F6;
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: #F8FAFC;
|
||||
}
|
||||
@@ -3353,6 +3376,19 @@ table#products {
|
||||
}
|
||||
}
|
||||
|
||||
.product-warning-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #DC2626;
|
||||
font-size: 16px;
|
||||
|
||||
&:hover {
|
||||
color: #B91C1C;
|
||||
}
|
||||
}
|
||||
|
||||
.products-row-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -160,6 +160,11 @@
|
||||
<div class="campaigns-extra-card-title">
|
||||
<i class="fa-solid fa-key"></i> Frazy dodane do kampanii/grup reklam
|
||||
</div>
|
||||
<div class="terms-keywords-toolbar">
|
||||
<button type="button" id="terms_add_keyword_btn" class="terms-negative-bulk-btn">
|
||||
<i class="fa-solid fa-plus"></i> Dodaj fraze
|
||||
</button>
|
||||
</div>
|
||||
<div class="campaigns-extra-table-wrap">
|
||||
<table class="table table-sm campaigns-extra-table" id="terms_keywords_table">
|
||||
<thead>
|
||||
@@ -167,6 +172,7 @@
|
||||
<th>Fraza</th>
|
||||
<th>Match type</th>
|
||||
<th>Grupa reklam</th>
|
||||
<th>Akcja</th>
|
||||
<th>Klik. all</th>
|
||||
<th>Koszt all</th>
|
||||
<th>Wartosc all</th>
|
||||
@@ -180,7 +186,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td colspan="13" class="campaigns-empty-row">Brak danych.</td></tr>
|
||||
<tr><td colspan="14" class="campaigns-empty-row">Brak danych.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1364,6 +1370,13 @@ function terms_clear_negative_selection()
|
||||
}
|
||||
|
||||
|
||||
var MATCH_TYPE_LABELS = { 'BROAD': 'Przyblizone', 'PHRASE': 'Do wyrazenia', 'EXACT': 'Scisle' };
|
||||
function render_match_type_label( data, type ) {
|
||||
if ( type !== 'display' ) return data || '';
|
||||
var val = String( data || '' ).toUpperCase();
|
||||
return MATCH_TYPE_LABELS[val] || val;
|
||||
}
|
||||
|
||||
function build_ad_groups_table( rows )
|
||||
{
|
||||
destroy_table_if_exists( '#terms_ad_groups_table' );
|
||||
@@ -1530,7 +1543,7 @@ function build_negative_terms_table( rows )
|
||||
return '<input type="checkbox" class="terms-negative-select-row" data-negative-keyword-id="' + negative_keyword_id + '" aria-label="Zaznacz fraze do usuniecia">';
|
||||
} },
|
||||
{ data: 'keyword_text', defaultContent: '', width: '520px' },
|
||||
{ data: 'match_type', defaultContent: '' },
|
||||
{ data: 'match_type', defaultContent: '', render: render_match_type_label },
|
||||
{ data: 'id', orderable: false, searchable: false, render: function( data, type ) {
|
||||
if ( type !== 'display' ) return data;
|
||||
return '<button type="button" class="terms-remove-negative-btn" data-negative-keyword-id="' + data + '" title="Usun z wykluczajacych"><i class="fa-solid fa-trash"></i></button>';
|
||||
@@ -1597,11 +1610,16 @@ function build_keywords_table( rows )
|
||||
lengthChange: false,
|
||||
pageLength: 15,
|
||||
pagingType: 'simple_numbers',
|
||||
order: [[ 3, 'desc' ]],
|
||||
order: [[ 4, 'desc' ]],
|
||||
columns: [
|
||||
{ data: 'keyword_text', defaultContent: '' },
|
||||
{ data: 'match_type', defaultContent: '' },
|
||||
{ data: 'match_type', defaultContent: '', render: render_match_type_label },
|
||||
{ data: 'ad_group_name', defaultContent: '-' },
|
||||
{ data: 'id', orderable: false, searchable: false, render: function( data, type, row ) {
|
||||
if ( type !== 'display' ) return data;
|
||||
return '<button type="button" class="terms-change-match-type-btn" data-keyword-id="' + data + '" title="Zmien dopasowanie"><i class="fa-solid fa-pen-to-square"></i></button>' +
|
||||
' <button type="button" class="terms-delete-keyword-btn" data-keyword-id="' + data + '" title="Usun fraze"><i class="fa-solid fa-trash"></i></button>';
|
||||
} },
|
||||
{ data: 'clicks_all_time', render: function( data, type ){ return type === 'display' ? format_num( data, 0 ) : Number( data || 0 ); } },
|
||||
{ data: 'cost_all_time', render: function( data, type ){ return type === 'display' ? format_num( data, 2 ) : Number( data || 0 ); } },
|
||||
{ data: 'conversion_value_all_time', render: function( data, type ){ return type === 'display' ? format_num( data, 2 ) : Number( data || 0 ); } },
|
||||
@@ -1619,10 +1637,11 @@ function build_keywords_table( rows )
|
||||
columnDefs: [
|
||||
{ targets: 0, className: 'text-cell phrase-nowrap', width: '220px' },
|
||||
{ targets: [1,2], className: 'text-cell', width: '140px' },
|
||||
{ targets: [3,8], className: 'num-cell', width: '70px' },
|
||||
{ targets: [4,5,9,10], className: 'num-cell', width: '85px' },
|
||||
{ targets: [6,11], className: 'num-cell', width: '80px' },
|
||||
{ targets: [7,12], className: 'num-cell', width: '130px' }
|
||||
{ targets: 3, className: 'dt-center', width: '90px' },
|
||||
{ targets: [4,9], className: 'num-cell', width: '70px' },
|
||||
{ targets: [5,6,10,11], className: 'num-cell', width: '85px' },
|
||||
{ targets: [7,12], className: 'num-cell', width: '80px' },
|
||||
{ targets: [8,13], className: 'num-cell', width: '130px' }
|
||||
],
|
||||
language: {
|
||||
emptyTable: 'Brak danych do wyswietlenia',
|
||||
@@ -2338,6 +2357,292 @@ $( function()
|
||||
terms_ad_groups_table.columns.adjust().draw( false );
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'click', '.terms-change-match-type-btn', function()
|
||||
{
|
||||
var keyword_id = parseInt( $( this ).data( 'keyword-id' ), 10 );
|
||||
var row_data = terms_keywords_table ? terms_keywords_table.row( $( this ).closest( 'tr' ) ).data() : null;
|
||||
var keyword_text = row_data ? String( row_data.keyword_text || '' ) : '';
|
||||
var current_match = row_data ? String( row_data.match_type || 'BROAD' ).toUpperCase() : 'BROAD';
|
||||
|
||||
if ( !keyword_id ) return;
|
||||
|
||||
$.confirm({
|
||||
title: 'Zmien dopasowanie frazy',
|
||||
columnClass: 'col-md-4 col-md-offset-4',
|
||||
content:
|
||||
'<div style="margin-bottom:10px;"><strong>' + $('<span>').text( keyword_text ).html() + '</strong></div>' +
|
||||
'<div class="form-group" style="margin-bottom:0;">' +
|
||||
'<label for="kw_new_match_type" style="display:block;margin-bottom:6px;">Nowy typ dopasowania</label>' +
|
||||
'<select id="kw_new_match_type" class="form-control">' +
|
||||
'<option value="BROAD"' + ( current_match === 'BROAD' ? ' selected' : '' ) + '>Dopasowanie przyblizone</option>' +
|
||||
'<option value="PHRASE"' + ( current_match === 'PHRASE' ? ' selected' : '' ) + '>Dopasowanie do wyrazenia</option>' +
|
||||
'<option value="EXACT"' + ( current_match === 'EXACT' ? ' selected' : '' ) + '>Dopasowanie scisle</option>' +
|
||||
'</select>' +
|
||||
'<small style="display:block;margin-top:6px;color:#64748B;">Obecne: ' + current_match + '. Zmiana usunie stary keyword i doda nowy w Google Ads.</small>' +
|
||||
'</div>',
|
||||
type: 'blue',
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: 'Zmien',
|
||||
btnClass: 'btn-blue',
|
||||
action: function()
|
||||
{
|
||||
var new_match_type = this.$content.find( '#kw_new_match_type' ).val() || 'BROAD';
|
||||
var modal = this;
|
||||
|
||||
if ( new_match_type === current_match )
|
||||
{
|
||||
$.alert({ title: 'Info', columnClass: 'col-md-4 col-md-offset-4', content: 'Wybrany typ dopasowania jest taki sam jak obecny.', type: 'blue' });
|
||||
return;
|
||||
}
|
||||
|
||||
modal.showLoading( true );
|
||||
|
||||
$.ajax({
|
||||
url: '/campaign_terms/update_keyword_match_type/',
|
||||
type: 'POST',
|
||||
data: { keyword_id: keyword_id, new_match_type: new_match_type },
|
||||
success: function( response )
|
||||
{
|
||||
var data = JSON.parse( response );
|
||||
modal.close();
|
||||
|
||||
if ( data.success )
|
||||
{
|
||||
var debugHtml = terms_build_debug_details_html( data.debug || null );
|
||||
var successDialog = $.alert({
|
||||
title: 'Sukces',
|
||||
columnClass: 'col-md-4 col-md-offset-4',
|
||||
content: ( data.message || 'Dopasowanie zmienione.' ) + debugHtml,
|
||||
type: 'green'
|
||||
});
|
||||
setTimeout( function() { successDialog.close(); }, 8000 );
|
||||
load_phrase_tables();
|
||||
}
|
||||
else
|
||||
{
|
||||
var debugHtml = terms_build_debug_details_html( data.debug || null );
|
||||
$.alert({
|
||||
title: 'Blad',
|
||||
columnClass: 'col-md-4 col-md-offset-4',
|
||||
content: ( data.message || 'Nie udalo sie zmienic dopasowania.' ) + ( data.error ? '<br><small>' + data.error + '</small>' : '' ) + debugHtml,
|
||||
type: 'red'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function()
|
||||
{
|
||||
modal.close();
|
||||
$.alert({ title: 'Blad', columnClass: 'col-md-4 col-md-offset-4', content: 'Blad komunikacji z serwerem.', type: 'red' });
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
cancel: { text: 'Anuluj' }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'click', '.terms-delete-keyword-btn', function()
|
||||
{
|
||||
var keyword_id = parseInt( $( this ).data( 'keyword-id' ), 10 );
|
||||
var row_data = terms_keywords_table ? terms_keywords_table.row( $( this ).closest( 'tr' ) ).data() : null;
|
||||
var keyword_text = row_data ? String( row_data.keyword_text || '' ) : '';
|
||||
|
||||
if ( !keyword_id ) return;
|
||||
|
||||
$.confirm({
|
||||
title: 'Usun fraze',
|
||||
columnClass: 'col-md-4 col-md-offset-4',
|
||||
content: 'Czy na pewno usunac fraze <strong>' + $('<span>').text( keyword_text ).html() + '</strong>? Operacja zostanie wyslana do Google Ads API.',
|
||||
type: 'red',
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: 'Usun',
|
||||
btnClass: 'btn-red',
|
||||
keys: [ 'enter' ],
|
||||
action: function()
|
||||
{
|
||||
var modal = this;
|
||||
modal.showLoading( true );
|
||||
|
||||
$.ajax({
|
||||
url: '/campaign_terms/delete_keyword/',
|
||||
type: 'POST',
|
||||
data: { keyword_id: keyword_id },
|
||||
success: function( response )
|
||||
{
|
||||
var data = JSON.parse( response );
|
||||
modal.close();
|
||||
var debugHtml = terms_build_debug_details_html( data.debug || null );
|
||||
|
||||
if ( data.success )
|
||||
{
|
||||
$.alert({
|
||||
title: 'Sukces',
|
||||
columnClass: 'col-md-4 col-md-offset-4',
|
||||
content: ( data.message || 'Fraza zostala usunieta.' ) + debugHtml,
|
||||
type: 'green'
|
||||
});
|
||||
load_phrase_tables();
|
||||
}
|
||||
else
|
||||
{
|
||||
$.alert({
|
||||
title: 'Blad',
|
||||
columnClass: 'col-md-4 col-md-offset-4',
|
||||
content: ( data.message || 'Nie udalo sie usunac frazy.' ) + ( data.error ? '<br><small>' + data.error + '</small>' : '' ) + debugHtml,
|
||||
type: 'red'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function()
|
||||
{
|
||||
modal.close();
|
||||
$.alert({ title: 'Blad', columnClass: 'col-md-4 col-md-offset-4', content: 'Blad komunikacji z serwerem.', type: 'red' });
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
cancel: { text: 'Anuluj' }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'click', '#terms_add_keyword_btn', function()
|
||||
{
|
||||
var campaign_id = $( '#terms_campaign_id' ).val();
|
||||
if ( !campaign_id )
|
||||
{
|
||||
$.alert({ title: 'Uwaga', columnClass: 'col-md-4 col-md-offset-4', content: 'Wybierz kampanie przed dodaniem frazy.', type: 'orange' });
|
||||
return;
|
||||
}
|
||||
|
||||
var ad_group_options = '';
|
||||
$( '#terms_ad_group_id option' ).each( function()
|
||||
{
|
||||
var val = $( this ).val();
|
||||
if ( val )
|
||||
ad_group_options += '<option value="' + val + '">' + $('<span>').text( $( this ).text() ).html() + '</option>';
|
||||
});
|
||||
|
||||
if ( !ad_group_options )
|
||||
{
|
||||
$.alert({ title: 'Uwaga', columnClass: 'col-md-4 col-md-offset-4', content: 'Brak grup reklam. Wybierz kampanie z grupami.', type: 'orange' });
|
||||
return;
|
||||
}
|
||||
|
||||
var selected_ad_group = $( '#terms_ad_group_id' ).val() || '';
|
||||
|
||||
$.confirm({
|
||||
title: 'Dodaj fraze do grupy reklam',
|
||||
columnClass: 'col-md-4 col-md-offset-4',
|
||||
content:
|
||||
'<div class="form-group" style="margin-bottom:10px;">' +
|
||||
'<label for="add_kw_text" style="display:block;margin-bottom:6px;">Fraza</label>' +
|
||||
'<input id="add_kw_text" type="text" class="form-control" placeholder="Wpisz fraze..." />' +
|
||||
'</div>' +
|
||||
'<div class="form-group" style="margin-bottom:10px;">' +
|
||||
'<label for="add_kw_match_type" style="display:block;margin-bottom:6px;">Typ dopasowania</label>' +
|
||||
'<select id="add_kw_match_type" class="form-control">' +
|
||||
'<option value="BROAD" selected>Dopasowanie przyblizone</option>' +
|
||||
'<option value="PHRASE">Dopasowanie do wyrazenia</option>' +
|
||||
'<option value="EXACT">Dopasowanie scisle</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="form-group" style="margin-bottom:0;">' +
|
||||
'<label for="add_kw_ad_group" style="display:block;margin-bottom:6px;">Grupa reklam</label>' +
|
||||
'<select id="add_kw_ad_group" class="form-control">' + ad_group_options + '</select>' +
|
||||
'</div>',
|
||||
type: 'blue',
|
||||
onContentReady: function()
|
||||
{
|
||||
if ( selected_ad_group )
|
||||
this.$content.find( '#add_kw_ad_group' ).val( selected_ad_group );
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: 'Dodaj',
|
||||
btnClass: 'btn-blue',
|
||||
action: function()
|
||||
{
|
||||
var keyword_text = $.trim( this.$content.find( '#add_kw_text' ).val() || '' );
|
||||
var match_type = this.$content.find( '#add_kw_match_type' ).val() || 'BROAD';
|
||||
var ad_group_id = this.$content.find( '#add_kw_ad_group' ).val() || '';
|
||||
var modal = this;
|
||||
|
||||
if ( keyword_text === '' )
|
||||
{
|
||||
$.alert({ title: 'Uwaga', columnClass: 'col-md-4 col-md-offset-4', content: 'Wpisz fraze do dodania.', type: 'orange' });
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !ad_group_id )
|
||||
{
|
||||
$.alert({ title: 'Uwaga', columnClass: 'col-md-4 col-md-offset-4', content: 'Wybierz grupe reklam.', type: 'orange' });
|
||||
return false;
|
||||
}
|
||||
|
||||
modal.showLoading( true );
|
||||
|
||||
$.ajax({
|
||||
url: '/campaign_terms/add_keyword/',
|
||||
type: 'POST',
|
||||
data: {
|
||||
campaign_id: campaign_id,
|
||||
ad_group_id: ad_group_id,
|
||||
keyword_text: keyword_text,
|
||||
match_type: match_type
|
||||
},
|
||||
success: function( response )
|
||||
{
|
||||
var data = JSON.parse( response );
|
||||
modal.close();
|
||||
|
||||
if ( data.success )
|
||||
{
|
||||
var debugHtml = terms_build_debug_details_html( data.debug || null );
|
||||
var successDialog = $.alert({
|
||||
title: 'Sukces',
|
||||
columnClass: 'col-md-4 col-md-offset-4',
|
||||
content: ( data.message || 'Fraza zostala dodana.' )
|
||||
+ '<br><small style="display:block;margin-top:8px;color:#64748B;">Zmiana moze byc widoczna w panelu Google Ads po 1-3 minutach.</small>'
|
||||
+ debugHtml,
|
||||
type: 'green'
|
||||
});
|
||||
setTimeout( function() { successDialog.close(); }, 10000 );
|
||||
load_phrase_tables();
|
||||
}
|
||||
else
|
||||
{
|
||||
var debugHtml = terms_build_debug_details_html( data.debug || null );
|
||||
$.alert({
|
||||
title: 'Blad',
|
||||
columnClass: 'col-md-4 col-md-offset-4',
|
||||
content: ( data.message || 'Nie udalo sie dodac frazy.' ) + ( data.error ? '<br><small>' + data.error + '</small>' : '' ) + debugHtml,
|
||||
type: 'red'
|
||||
});
|
||||
}
|
||||
},
|
||||
error: function()
|
||||
{
|
||||
modal.close();
|
||||
$.alert({ title: 'Blad', columnClass: 'col-md-4 col-md-offset-4', content: 'Blad komunikacji z serwerem.', type: 'red' });
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
cancel: { text: 'Anuluj' }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
terms_restore_ad_groups_collapsed();
|
||||
reset_all_tables();
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
<th>Grupa reklam</th>
|
||||
<th>URL</th>
|
||||
<th>Nazwa produktu</th>
|
||||
<th title="Ostrzezenia produktowe"><i class="fa-solid fa-triangle-exclamation"></i></th>
|
||||
<th>Wyśw.</th>
|
||||
<th>Wyśw. (30d)</th>
|
||||
<th>Klik.</th>
|
||||
@@ -119,7 +120,7 @@ var AI_OPENAI_ENABLED = <?= $openai_enabled ? 'true' : 'false'; ?>;
|
||||
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, 20 ];
|
||||
var PRODUCTS_LOCKED_COLUMNS = [ 0, 21 ];
|
||||
|
||||
function show_toast( message, type )
|
||||
{
|
||||
@@ -347,6 +348,7 @@ $( function()
|
||||
{ width: '200px', name: 'ad_group_name' },
|
||||
{ width: '120px', orderable: false, searchable: false },
|
||||
{ name: 'name' },
|
||||
{ width: '35px', orderable: false, searchable: false, className: 'dt-center' },
|
||||
{ width: '50px', name: 'impressions' },
|
||||
{ width: '80px', name: 'impressions_30' },
|
||||
{ width: '50px', name: 'clicks' },
|
||||
@@ -363,12 +365,12 @@ $( function()
|
||||
{ width: '190px', orderable: false, className: 'dt-center' }
|
||||
],
|
||||
createdRow: function( row, data ) {
|
||||
var cl4Val = $( data[19] ).val();
|
||||
var cl4Val = $( data[20] ).val();
|
||||
if ( cl4Val && cl4Val.toLowerCase() === 'niedostępny' ) {
|
||||
$( row ).addClass( 'product-row-unavailable' );
|
||||
}
|
||||
},
|
||||
order: [ [ 9, 'desc' ] ],
|
||||
order: [ [ 10, 'desc' ] ],
|
||||
language: {
|
||||
processing: 'Ładowanie...',
|
||||
emptyTable: 'Brak produktów do wyświetlenia',
|
||||
@@ -1826,6 +1828,28 @@ $( function()
|
||||
updateSelectedCount();
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'click', '.product-warning-icon', function()
|
||||
{
|
||||
var warnings = $( this ).data( 'warnings' ) || '';
|
||||
if ( !warnings ) return;
|
||||
|
||||
var lines = String( warnings ).split( '\n' );
|
||||
var html = '<ul style="text-align:left;padding-left:18px;margin:0;">';
|
||||
for ( var i = 0; i < lines.length; i++ )
|
||||
{
|
||||
if ( $.trim( lines[i] ) !== '' )
|
||||
html += '<li style="margin-bottom:6px;">' + $('<span>').text( lines[i] ).html() + '</li>';
|
||||
}
|
||||
html += '</ul>';
|
||||
|
||||
$.alert({
|
||||
title: 'Ostrzezenia produktu',
|
||||
columnClass: 'col-md-5 col-md-offset-4',
|
||||
content: html,
|
||||
type: 'orange'
|
||||
});
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'change', '.products-col-toggle', function()
|
||||
{
|
||||
var col_index = Number( $( this ).data( 'col-index' ) );
|
||||
|
||||
Reference in New Issue
Block a user