feat: update font to Roboto across templates and add campaign/ad group filters in product views
- Changed font from Open Sans to Roboto in layout files. - Added campaign and ad group filters in products main view. - Enhanced product history to include campaign and ad group IDs. - Updated migrations to support new campaign and ad group dimensions in product statistics. - Introduced new migration files for managing campaign types and dropping obsolete columns.
This commit is contained in:
138
.vscode/ftp-kr.sync.cache.json
vendored
138
.vscode/ftp-kr.sync.cache.json
vendored
@@ -120,11 +120,17 @@
|
||||
".claude": {
|
||||
"settings.local.json": {
|
||||
"type": "-",
|
||||
"size": 381,
|
||||
"lmtime": 1771198236725,
|
||||
"size": 528,
|
||||
"lmtime": 1771368558172,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"CLAUDE.md": {
|
||||
"type": "-",
|
||||
"size": 4139,
|
||||
"lmtime": 1771368527045,
|
||||
"modified": false
|
||||
},
|
||||
"config.php": {
|
||||
"type": "-",
|
||||
"size": 357,
|
||||
@@ -137,7 +143,38 @@
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"docs": {},
|
||||
"docs": {
|
||||
"database.sql": {
|
||||
"type": "-",
|
||||
"size": 15919,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"google_ads_api_design_doc.doc": {
|
||||
"type": "-",
|
||||
"size": 6924,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"google_ads_api_design_doc.md": {
|
||||
"type": "-",
|
||||
"size": 5067,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"PLAN.md": {
|
||||
"type": "-",
|
||||
"size": 11544,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"memory.md": {
|
||||
"type": "-",
|
||||
"size": 1697,
|
||||
"lmtime": 1771368520970,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
".htaccess": {
|
||||
"type": "-",
|
||||
"size": 601,
|
||||
@@ -165,14 +202,14 @@
|
||||
},
|
||||
"style.css": {
|
||||
"type": "-",
|
||||
"size": 34772,
|
||||
"lmtime": 1771198851660,
|
||||
"size": 35676,
|
||||
"lmtime": 1771367686453,
|
||||
"modified": false
|
||||
},
|
||||
"style.css.map": {
|
||||
"type": "-",
|
||||
"size": 42515,
|
||||
"lmtime": 1771169108538,
|
||||
"size": 8131,
|
||||
"lmtime": 1771367686452,
|
||||
"modified": false
|
||||
},
|
||||
"style-old.css": {
|
||||
@@ -189,12 +226,36 @@
|
||||
},
|
||||
"style.scss": {
|
||||
"type": "-",
|
||||
"size": 37864,
|
||||
"lmtime": 1771198844499,
|
||||
"size": 38739,
|
||||
"lmtime": 1771367615645,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"adspro-dialog.js": {
|
||||
"type": "-",
|
||||
"size": 9505,
|
||||
"lmtime": 1771367545298,
|
||||
"modified": false
|
||||
},
|
||||
"bootstrap": {},
|
||||
"bootstrap-4.1.3": {},
|
||||
"ckeditor": {},
|
||||
"countdown": {},
|
||||
"datepicker": {},
|
||||
"daterange": {},
|
||||
"filemanager-9.14.1": {},
|
||||
"font-awesome-4.7.0": {},
|
||||
"framework": {},
|
||||
"functions.js": {
|
||||
"type": "-",
|
||||
"size": 3400,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"grid": {},
|
||||
"icheck-1.0.2": {},
|
||||
"jquery": {},
|
||||
"jquery-confirm": {
|
||||
"jquery-confirm.min.css": {
|
||||
"type": "-",
|
||||
@@ -208,6 +269,37 @@
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"jquery.contextMenu": {},
|
||||
"jquery-gantt": {},
|
||||
"medoo": {},
|
||||
"moment": {},
|
||||
"phpmailer": {},
|
||||
"rb.php": {
|
||||
"type": "-",
|
||||
"size": 546666,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"select2": {},
|
||||
"tagsinput": {},
|
||||
"typeahead.bundle.js": {
|
||||
"type": "-",
|
||||
"size": 96186,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"xlsxwriter.class.php": {
|
||||
"type": "-",
|
||||
"size": 48385,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"adspro-dialog.css": {
|
||||
"type": "-",
|
||||
"size": 6801,
|
||||
"lmtime": 1771367581706,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"migrations": {
|
||||
@@ -254,21 +346,21 @@
|
||||
"site": {
|
||||
"layout-cron.php": {
|
||||
"type": "-",
|
||||
"size": 5804,
|
||||
"lmtime": 0,
|
||||
"size": 5764,
|
||||
"lmtime": 1771367592957,
|
||||
"modified": false
|
||||
},
|
||||
"layout-logged.php": {
|
||||
"type": "-",
|
||||
"size": 4944,
|
||||
"lmtime": 1763678430048,
|
||||
"size": 7746,
|
||||
"lmtime": 1771367592216,
|
||||
"modified": false
|
||||
},
|
||||
"layout-unlogged.php": {
|
||||
"type": "-",
|
||||
"size": 1056,
|
||||
"size": 2024,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
"modified": true
|
||||
}
|
||||
},
|
||||
"campaigns": {
|
||||
@@ -292,12 +384,26 @@
|
||||
"lmtime": 1771198773079,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
"campaign_terms": {
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 25469,
|
||||
"lmtime": 1771367807171,
|
||||
"modified": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"tmp": {},
|
||||
"tools": {},
|
||||
"upload": {},
|
||||
"xml": {}
|
||||
"xml": {},
|
||||
".gitignore": {
|
||||
"type": "-",
|
||||
"size": 16,
|
||||
"lmtime": 1771368579889,
|
||||
"modified": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"$version": 1
|
||||
|
||||
45
AGENTS.md
Normal file
45
AGENTS.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
adsPRO is a custom PHP MVC app. Keep business logic in:
|
||||
- `autoload/controls/` (`class.*.php`): request handling (`\controls\*`)
|
||||
- `autoload/factory/` (`class.*.php`): DB access via Medoo (`\factory\*`)
|
||||
- `autoload/view/` (`class.*.php`): view composition (`\view\*`)
|
||||
- `autoload/services/`: external integrations (Google Ads, OpenAI, Claude)
|
||||
- `templates/`: PHP templates rendered with `\Tpl::view()`
|
||||
- `migrations/`: SQL migrations (`NNN_description.sql` + optional `demo_data.sql`)
|
||||
- `layout/`: SCSS/CSS assets (`style.scss` -> `style.css`)
|
||||
|
||||
Main entry points are `index.php`, `ajax.php`, `api.php`, `cron.php`, and `install.php`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- `php -S 127.0.0.1:8000` - run a local PHP server from repo root.
|
||||
- `php install.php` - apply pending DB migrations.
|
||||
- `php install.php --with_demo` - apply migrations and demo data.
|
||||
- `php install.php --force` - re-apply tracked migrations.
|
||||
- `php -l autoload/controls/class.Campaigns.php` - lint a changed PHP file.
|
||||
|
||||
There is no dedicated build pipeline; frontend dependencies are committed in `libraries/`. SCSS is typically compiled via VS Code Live Sass Compiler.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Follow existing PHP style: spaces inside parentheses (`if ( $x )`), braces on new lines.
|
||||
- Use `PascalCase` class names, lowercase namespaces (`\controls`, `\factory`), and `snake_case` for methods/variables/DB columns.
|
||||
- Controllers/factories are conventionally `static public function ...`.
|
||||
- Keep route/module naming aligned with URL pattern `/module/action/...`.
|
||||
|
||||
## Testing Guidelines
|
||||
No first-party automated test suite is maintained in this repo. Validate changes with:
|
||||
- PHP lint on edited files.
|
||||
- Manual UI checks for affected `templates/` views.
|
||||
- Endpoint checks for `ajax.php`, `api.php`, or `cron.php` paths you touched.
|
||||
- Migration dry run on a non-production database when schema is changed.
|
||||
|
||||
Document manual test steps and outcomes in each PR.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Use concise Polish commit messages with prefixes seen in history: `feat:`, `fix:`, `update:`.
|
||||
- Keep commits focused (feature, refactor, migration, UI tweak).
|
||||
- PRs should include: scope summary, linked issue/task, migration notes, manual test checklist, and screenshots for UI changes.
|
||||
|
||||
## Security & Configuration Tips
|
||||
`config.php` contains environment credentials. Do not introduce new secrets in commits or PR discussions, and treat any credential change as a coordinated ops task.
|
||||
@@ -295,7 +295,7 @@ class Api
|
||||
|
||||
$processed = 0;
|
||||
$skipped = 0;
|
||||
$touched_product_ids = [];
|
||||
$touched_scopes = [];
|
||||
|
||||
foreach ( $data['data'] as $offer )
|
||||
{
|
||||
@@ -338,6 +338,23 @@ class Api
|
||||
continue;
|
||||
}
|
||||
|
||||
$campaign_external_id = (int) self::normalize_number( $offer['CampaignId'] ?? ( $offer['campaign_id'] ?? 0 ) );
|
||||
$campaign_name = trim( (string) ( $offer['CampaignName'] ?? ( $offer['campaign_name'] ?? '' ) ) );
|
||||
$ad_group_external_id = (int) self::normalize_number( $offer['AdGroupId'] ?? ( $offer['ad_group_id'] ?? 0 ) );
|
||||
$ad_group_name = trim( (string) ( $offer['AdGroupName'] ?? ( $offer['ad_group_name'] ?? '' ) ) );
|
||||
|
||||
$scope = \controls\Cron::resolve_products_scope_ids(
|
||||
$client_id,
|
||||
$campaign_external_id,
|
||||
$campaign_name,
|
||||
$ad_group_external_id,
|
||||
$ad_group_name,
|
||||
$date
|
||||
);
|
||||
|
||||
$db_campaign_id = (int) ( $scope['campaign_id'] ?? 0 );
|
||||
$db_ad_group_id = (int) ( $scope['ad_group_id'] ?? 0 );
|
||||
|
||||
$impressions = (int) round( self::normalize_number( $offer['Impressions'] ?? 0 ) );
|
||||
$clicks = (int) round( self::normalize_number( $offer['Clicks'] ?? 0 ) );
|
||||
$cost = self::normalize_number( $offer['Cost'] ?? 0 );
|
||||
@@ -352,12 +369,24 @@ class Api
|
||||
'cost' => $cost,
|
||||
'conversions' => $conversions,
|
||||
'conversions_value' => $conversion_value,
|
||||
'updated' => 1
|
||||
'updated' => 1,
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $db_ad_group_id
|
||||
];
|
||||
|
||||
if ( $mdb -> count( 'products_history', [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] ) )
|
||||
if ( $mdb -> count( 'products_history', [ 'AND' => [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $db_ad_group_id,
|
||||
'date_add' => $date
|
||||
] ] ) )
|
||||
{
|
||||
$offer_data_old = $mdb -> get( 'products_history', '*', [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] );
|
||||
$offer_data_old = $mdb -> get( 'products_history', '*', [ 'AND' => [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $db_ad_group_id,
|
||||
'date_add' => $date
|
||||
] ] );
|
||||
|
||||
if (
|
||||
$offer_data_old['impressions'] == $offer_data['impressions']
|
||||
@@ -367,13 +396,23 @@ class Api
|
||||
and number_format( (float) str_replace( ',', '.', $offer_data_old['conversions_value'] ), 5 ) == number_format( (float) $offer_data['conversions_value'], 5 )
|
||||
)
|
||||
{
|
||||
$touched_product_ids[ $product_id ] = true;
|
||||
$scope_key = (int) $product_id . '|' . $db_campaign_id . '|' . $db_ad_group_id;
|
||||
$touched_scopes[ $scope_key ] = [
|
||||
'product_id' => (int) $product_id,
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $db_ad_group_id
|
||||
];
|
||||
$processed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$mdb -> update( 'products_history', $offer_data, [
|
||||
'AND' => [ 'product_id' => $product_id, 'date_add' => $date ]
|
||||
'AND' => [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $db_ad_group_id,
|
||||
'date_add' => $date
|
||||
]
|
||||
] );
|
||||
}
|
||||
else
|
||||
@@ -383,19 +422,33 @@ class Api
|
||||
$mdb -> insert( 'products_history', $offer_data );
|
||||
}
|
||||
|
||||
$touched_product_ids[ $product_id ] = true;
|
||||
$scope_key = (int) $product_id . '|' . $db_campaign_id . '|' . $db_ad_group_id;
|
||||
$touched_scopes[ $scope_key ] = [
|
||||
'product_id' => (int) $product_id,
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $db_ad_group_id
|
||||
];
|
||||
$processed++;
|
||||
}
|
||||
|
||||
$history_30_rows = 0;
|
||||
foreach ( array_keys( $touched_product_ids ) as $product_id )
|
||||
foreach ( $touched_scopes as $scope )
|
||||
{
|
||||
\controls\Cron::cron_product_history_30_save( (int) $product_id, $date );
|
||||
$mdb -> update( 'products_history', [ 'updated' => 0 ], [ 'AND' => [ 'product_id' => (int) $product_id, 'date_add' => $date ] ] );
|
||||
$product_id = (int) ( $scope['product_id'] ?? 0 );
|
||||
$campaign_id = (int) ( $scope['campaign_id'] ?? 0 );
|
||||
$ad_group_id = (int) ( $scope['ad_group_id'] ?? 0 );
|
||||
|
||||
\controls\Cron::cron_product_history_30_save( $product_id, $date, $campaign_id, $ad_group_id );
|
||||
$mdb -> update( 'products_history', [ 'updated' => 0 ], [ 'AND' => [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_group_id' => $ad_group_id,
|
||||
'date_add' => $date
|
||||
] ] );
|
||||
$history_30_rows++;
|
||||
}
|
||||
|
||||
$temp_rows = self::rebuild_products_temp_for_client( $client_id );
|
||||
$temp_rows = \controls\Cron::rebuild_products_temp_for_client( $client_id );
|
||||
|
||||
echo json_encode( [
|
||||
'status' => 'ok',
|
||||
@@ -411,67 +464,7 @@ class Api
|
||||
|
||||
static private function rebuild_products_temp_for_client( $client_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$client_id = (int) $client_id;
|
||||
if ( $client_id <= 0 )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
$product_ids = $mdb -> select( 'products', 'id', [ 'client_id' => $client_id ] );
|
||||
if ( empty( $product_ids ) )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
$mdb -> delete( 'products_temp', [ 'product_id' => $product_ids ] );
|
||||
|
||||
$rows = $mdb -> query(
|
||||
'SELECT p.id AS product_id, p.name,
|
||||
COALESCE( SUM( ph.impressions ), 0 ) AS impressions,
|
||||
COALESCE( SUM( ph.clicks ), 0 ) AS clicks,
|
||||
COALESCE( SUM( ph.cost ), 0 ) AS cost,
|
||||
COALESCE( SUM( ph.conversions ), 0 ) AS conversions,
|
||||
COALESCE( SUM( ph.conversions_value ), 0 ) AS conversions_value
|
||||
FROM products AS p
|
||||
LEFT JOIN products_history AS ph ON p.id = ph.product_id
|
||||
WHERE p.client_id = :client_id
|
||||
GROUP BY p.id, p.name',
|
||||
[ ':client_id' => $client_id ]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$inserted = 0;
|
||||
foreach ( $rows as $row )
|
||||
{
|
||||
$impressions = (int) $row['impressions'];
|
||||
$clicks = (int) $row['clicks'];
|
||||
$cost = (float) $row['cost'];
|
||||
$conversions = (float) $row['conversions'];
|
||||
$conversion_value = (float) $row['conversions_value'];
|
||||
$ctr = ( $impressions > 0 ) ? round( $clicks / $impressions, 4 ) * 100 : 0;
|
||||
$cpc = ( $clicks > 0 ) ? round( $cost / $clicks, 6 ) : 0;
|
||||
$roas = ( $cost > 0 ) ? round( $conversion_value / $cost, 2 ) * 100 : 0;
|
||||
|
||||
$mdb -> insert( 'products_temp', [
|
||||
'product_id' => (int) $row['product_id'],
|
||||
'name' => $row['name'],
|
||||
'impressions' => $impressions,
|
||||
'impressions_30' => (int) \factory\Products::get_impressions_30( (int) $row['product_id'] ),
|
||||
'clicks' => $clicks,
|
||||
'clicks_30' => (int) \factory\Products::get_clicks_30( (int) $row['product_id'] ),
|
||||
'ctr' => $ctr,
|
||||
'cost' => $cost,
|
||||
'conversions' => $conversions,
|
||||
'conversions_value' => $conversion_value,
|
||||
'cpc' => $cpc,
|
||||
'roas' => $roas
|
||||
] );
|
||||
|
||||
$inserted++;
|
||||
}
|
||||
|
||||
return $inserted;
|
||||
return \controls\Cron::rebuild_products_temp_for_client( $client_id );
|
||||
}
|
||||
|
||||
static private function normalize_number( $value )
|
||||
|
||||
@@ -13,7 +13,7 @@ class CampaignTerms
|
||||
static public function get_campaigns_list()
|
||||
{
|
||||
$client_id = (int) \S::get( 'client_id' );
|
||||
echo json_encode( [ 'campaigns' => \factory\Campaigns::get_campaigns_list( $client_id ) ] );
|
||||
echo json_encode( [ 'campaigns' => \factory\Campaigns::get_campaigns_list( $client_id, true ) ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ class CampaignTerms
|
||||
$search_term_id = (int) \S::get( 'search_term_id' );
|
||||
$match_type = strtoupper( trim( (string) \S::get( 'match_type' ) ) );
|
||||
$scope = strtolower( trim( (string) \S::get( 'scope' ) ) );
|
||||
$manual_keyword_text = trim( (string) \S::get( 'keyword_text' ) );
|
||||
|
||||
if ( $search_term_id <= 0 )
|
||||
{
|
||||
@@ -80,7 +81,9 @@ class CampaignTerms
|
||||
$customer_id = trim( (string) ( $context['google_ads_customer_id'] ?? '' ) );
|
||||
$campaign_external_id = trim( (string) ( $context['external_campaign_id'] ?? '' ) );
|
||||
$ad_group_external_id = trim( (string) ( $context['external_ad_group_id'] ?? '' ) );
|
||||
$keyword_text = trim( (string) ( $context['search_term'] ?? '' ) );
|
||||
$context_keyword_text = trim( (string) ( $context['search_term'] ?? '' ) );
|
||||
$keyword_text = $manual_keyword_text !== '' ? $manual_keyword_text : $context_keyword_text;
|
||||
$keyword_source = $manual_keyword_text !== '' ? 'manual' : 'search_term';
|
||||
|
||||
$missing_data = ( $customer_id === '' || $keyword_text === '' );
|
||||
if ( $scope === 'campaign' && $campaign_external_id === '' )
|
||||
@@ -102,6 +105,7 @@ class CampaignTerms
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'keyword_source' => $keyword_source,
|
||||
'scope' => $scope,
|
||||
'context' => $context
|
||||
]
|
||||
@@ -137,6 +141,7 @@ class CampaignTerms
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'keyword_source' => $keyword_source,
|
||||
'match_type' => $match_type,
|
||||
'scope' => $scope,
|
||||
'api_result' => $api_result
|
||||
@@ -166,6 +171,7 @@ class CampaignTerms
|
||||
'campaign_external_id' => $campaign_external_id,
|
||||
'ad_group_external_id' => $ad_group_external_id,
|
||||
'keyword_text' => $keyword_text,
|
||||
'keyword_source' => $keyword_source,
|
||||
'scope' => $scope,
|
||||
'api_response' => $api_result['response'] ?? null,
|
||||
'sent_operation' => $api_result['sent_operation'] ?? null,
|
||||
|
||||
@@ -293,6 +293,23 @@ class Cron
|
||||
continue;
|
||||
}
|
||||
|
||||
$campaign_external_id = (int) ( $offer['CampaignId'] ?? 0 );
|
||||
$campaign_name = trim( (string) ( $offer['CampaignName'] ?? '' ) );
|
||||
$ad_group_external_id = (int) ( $offer['AdGroupId'] ?? 0 );
|
||||
$ad_group_name = trim( (string) ( $offer['AdGroupName'] ?? '' ) );
|
||||
|
||||
$scope = self::resolve_products_scope_ids(
|
||||
$client_id,
|
||||
$campaign_external_id,
|
||||
$campaign_name,
|
||||
$ad_group_external_id,
|
||||
$ad_group_name,
|
||||
$date
|
||||
);
|
||||
|
||||
$db_campaign_id = (int) ( $scope['campaign_id'] ?? 0 );
|
||||
$db_ad_group_id = (int) ( $scope['ad_group_id'] ?? 0 );
|
||||
|
||||
$impressions = (int) round( (float) ( $offer['Impressions'] ?? 0 ) );
|
||||
$clicks = (int) round( (float) ( $offer['Clicks'] ?? 0 ) );
|
||||
$cost = (float) ( $offer['Cost'] ?? 0 );
|
||||
@@ -307,12 +324,24 @@ class Cron
|
||||
'cost' => $cost,
|
||||
'conversions' => $conversions,
|
||||
'conversions_value' => $conversion_value,
|
||||
'updated' => 1
|
||||
'updated' => 1,
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $db_ad_group_id
|
||||
];
|
||||
|
||||
if ( $mdb -> count( 'products_history', [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] ) )
|
||||
if ( $mdb -> count( 'products_history', [ 'AND' => [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $db_ad_group_id,
|
||||
'date_add' => $date
|
||||
] ] ) )
|
||||
{
|
||||
$offer_data_old = $mdb -> get( 'products_history', '*', [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] );
|
||||
$offer_data_old = $mdb -> get( 'products_history', '*', [ 'AND' => [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $db_ad_group_id,
|
||||
'date_add' => $date
|
||||
] ] );
|
||||
|
||||
if (
|
||||
$offer_data_old['impressions'] == $offer_data['impressions']
|
||||
@@ -328,7 +357,12 @@ class Cron
|
||||
}
|
||||
|
||||
$mdb -> update( 'products_history', $offer_data, [
|
||||
'AND' => [ 'product_id' => $product_id, 'date_add' => $date ]
|
||||
'AND' => [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $db_ad_group_id,
|
||||
'date_add' => $date
|
||||
]
|
||||
] );
|
||||
}
|
||||
else
|
||||
@@ -351,6 +385,163 @@ class Cron
|
||||
];
|
||||
}
|
||||
|
||||
static public function resolve_products_scope_ids( $client_id, $campaign_external_id, $campaign_name, $ad_group_external_id, $ad_group_name, $date_sync )
|
||||
{
|
||||
$client_id = (int) $client_id;
|
||||
$campaign_external_id = (int) $campaign_external_id;
|
||||
$ad_group_external_id = (int) $ad_group_external_id;
|
||||
|
||||
$db_campaign_id = self::ensure_products_campaign(
|
||||
$client_id,
|
||||
$campaign_external_id,
|
||||
$campaign_name,
|
||||
$date_sync
|
||||
);
|
||||
|
||||
if ( $db_campaign_id <= 0 )
|
||||
{
|
||||
$db_campaign_id = self::ensure_products_campaign(
|
||||
$client_id,
|
||||
0,
|
||||
'--- konto ---',
|
||||
$date_sync
|
||||
);
|
||||
}
|
||||
|
||||
$db_ad_group_id = self::ensure_products_ad_group(
|
||||
$db_campaign_id,
|
||||
$ad_group_external_id,
|
||||
$ad_group_name,
|
||||
$date_sync
|
||||
);
|
||||
|
||||
return [
|
||||
'campaign_id' => (int) $db_campaign_id,
|
||||
'ad_group_id' => (int) $db_ad_group_id
|
||||
];
|
||||
}
|
||||
|
||||
static private function ensure_products_campaign( $client_id, $campaign_external_id, $campaign_name, $date_sync )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$client_id = (int) $client_id;
|
||||
$campaign_external_id = (int) $campaign_external_id;
|
||||
$campaign_name = trim( (string) $campaign_name );
|
||||
|
||||
if ( $client_id <= 0 )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
$db_campaign_id = (int) $mdb -> get( 'campaigns', 'id', [ 'AND' => [
|
||||
'client_id' => $client_id,
|
||||
'campaign_id' => $campaign_external_id
|
||||
] ] );
|
||||
|
||||
if ( $db_campaign_id > 0 )
|
||||
{
|
||||
if ( $campaign_name !== '' )
|
||||
{
|
||||
$mdb -> update( 'campaigns', [ 'campaign_name' => $campaign_name ], [ 'id' => $db_campaign_id ] );
|
||||
}
|
||||
|
||||
return $db_campaign_id;
|
||||
}
|
||||
|
||||
if ( $campaign_name === '' )
|
||||
{
|
||||
$campaign_name = $campaign_external_id > 0 ? 'Kampania #' . $campaign_external_id : '--- konto ---';
|
||||
}
|
||||
|
||||
$mdb -> insert( 'campaigns', [
|
||||
'client_id' => $client_id,
|
||||
'campaign_id' => $campaign_external_id,
|
||||
'campaign_name' => $campaign_name
|
||||
] );
|
||||
|
||||
$db_campaign_id = (int) $mdb -> id();
|
||||
|
||||
if ( $db_campaign_id > 0 && $date_sync )
|
||||
{
|
||||
if ( !$mdb -> count( 'campaigns_history', [ 'AND' => [ 'campaign_id' => $db_campaign_id, 'date_add' => $date_sync ] ] ) )
|
||||
{
|
||||
$mdb -> insert( 'campaigns_history', [
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'roas_30_days' => 0,
|
||||
'roas_all_time' => 0,
|
||||
'budget' => 0,
|
||||
'money_spent' => 0,
|
||||
'conversion_value' => 0,
|
||||
'bidding_strategy' => '',
|
||||
'date_add' => $date_sync
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
return $db_campaign_id;
|
||||
}
|
||||
|
||||
static private function ensure_products_ad_group( $db_campaign_id, $ad_group_external_id, $ad_group_name, $date_sync )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$db_campaign_id = (int) $db_campaign_id;
|
||||
$ad_group_external_id = (int) $ad_group_external_id;
|
||||
$ad_group_name = trim( (string) $ad_group_name );
|
||||
|
||||
if ( $db_campaign_id <= 0 )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ( $ad_group_external_id <= 0 )
|
||||
{
|
||||
return (int) self::ensure_campaign_level_ad_group( $db_campaign_id, $date_sync );
|
||||
}
|
||||
|
||||
$db_ad_group_id = (int) $mdb -> get( 'campaign_ad_groups', 'id', [ 'AND' => [
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $ad_group_external_id
|
||||
] ] );
|
||||
|
||||
if ( $db_ad_group_id > 0 )
|
||||
{
|
||||
if ( $ad_group_name !== '' )
|
||||
{
|
||||
$mdb -> update( 'campaign_ad_groups', [ 'ad_group_name' => $ad_group_name ], [ 'id' => $db_ad_group_id ] );
|
||||
}
|
||||
|
||||
return $db_ad_group_id;
|
||||
}
|
||||
|
||||
if ( $ad_group_name === '' )
|
||||
{
|
||||
$ad_group_name = 'Ad group #' . $ad_group_external_id;
|
||||
}
|
||||
|
||||
$mdb -> insert( 'campaign_ad_groups', [
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => $ad_group_external_id,
|
||||
'ad_group_name' => $ad_group_name,
|
||||
'impressions_30' => 0,
|
||||
'clicks_30' => 0,
|
||||
'cost_30' => 0,
|
||||
'conversions_30' => 0,
|
||||
'conversion_value_30' => 0,
|
||||
'roas_30' => 0,
|
||||
'impressions_all_time' => 0,
|
||||
'clicks_all_time' => 0,
|
||||
'cost_all_time' => 0,
|
||||
'conversions_all_time' => 0,
|
||||
'conversion_value_all_time' => 0,
|
||||
'roas_all_time' => 0,
|
||||
'date_sync' => $date_sync
|
||||
] );
|
||||
|
||||
return (int) $mdb -> id();
|
||||
}
|
||||
|
||||
static private function aggregate_products_history_30_for_client( $client_id, $date )
|
||||
{
|
||||
global $mdb;
|
||||
@@ -359,156 +550,200 @@ class Cron
|
||||
$date = date( 'Y-m-d', strtotime( $date ) );
|
||||
|
||||
$rows = $mdb -> query(
|
||||
'SELECT ph.id, ph.product_id, ph.date_add
|
||||
'SELECT DISTINCT ph.product_id, ph.campaign_id, ph.ad_group_id, ph.date_add
|
||||
FROM products_history AS ph
|
||||
INNER JOIN products AS p ON p.id = ph.product_id
|
||||
WHERE p.client_id = ' . $client_id . '
|
||||
AND ph.updated = 1
|
||||
AND ph.date_add = \'' . $date . '\'
|
||||
ORDER BY ph.product_id ASC'
|
||||
ORDER BY ph.product_id ASC, ph.campaign_id ASC, ph.ad_group_id ASC'
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$processed = 0;
|
||||
foreach ( $rows as $row )
|
||||
{
|
||||
$product_id = (int) $row['product_id'];
|
||||
self::cron_product_history_30_save( $product_id, $row['date_add'] );
|
||||
$mdb -> update( 'products_history', [ 'updated' => 0 ], [ 'id' => (int) $row['id'] ] );
|
||||
$campaign_id = (int) ( $row['campaign_id'] ?? 0 );
|
||||
$ad_group_id = (int) ( $row['ad_group_id'] ?? 0 );
|
||||
|
||||
self::cron_product_history_30_save( $product_id, $row['date_add'], $campaign_id, $ad_group_id );
|
||||
$mdb -> query(
|
||||
'UPDATE products_history AS ph
|
||||
INNER JOIN products AS p ON p.id = ph.product_id
|
||||
SET ph.updated = 0
|
||||
WHERE ph.product_id = :product_id
|
||||
AND ph.campaign_id = :campaign_id
|
||||
AND ph.ad_group_id = :ad_group_id
|
||||
AND ph.date_add = :date_add
|
||||
AND p.client_id = :client_id',
|
||||
[
|
||||
':product_id' => $product_id,
|
||||
':campaign_id' => $campaign_id,
|
||||
':ad_group_id' => $ad_group_id,
|
||||
':date_add' => $row['date_add'],
|
||||
':client_id' => $client_id
|
||||
]
|
||||
);
|
||||
$processed++;
|
||||
}
|
||||
|
||||
return $processed;
|
||||
}
|
||||
|
||||
static private function rebuild_products_temp_for_client( $client_id )
|
||||
static public function rebuild_products_temp_for_client( $client_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$client_bestseller_min_roas = \factory\Products::get_client_bestseller_min_roas( $client_id );
|
||||
|
||||
$db_result = $mdb -> query( 'SELECT * FROM products AS p INNER JOIN products_history AS ph ON p.id = ph.product_id WHERE p.client_id = ' . (int) $client_id ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$aggregated_data = [];
|
||||
|
||||
foreach ( $db_result as $row )
|
||||
$client_id = (int) $client_id;
|
||||
if ( $client_id <= 0 )
|
||||
{
|
||||
$product_id = (int) $row['product_id'];
|
||||
|
||||
if ( !isset( $aggregated_data[$client_id] ) )
|
||||
{
|
||||
$aggregated_data[$client_id] = [];
|
||||
}
|
||||
|
||||
if ( !isset( $aggregated_data[$client_id][$product_id] ) )
|
||||
{
|
||||
$aggregated_data[$client_id][$product_id] = [
|
||||
'product_id' => $product_id,
|
||||
'name' => $row['name'],
|
||||
'impressions' => 0,
|
||||
'clicks' => 0,
|
||||
'cost' => 0.0,
|
||||
'conversions' => 0,
|
||||
'conversions_value' => 0.0
|
||||
];
|
||||
}
|
||||
|
||||
$aggregated_data[$client_id][$product_id]['impressions'] += (int) $row['impressions'];
|
||||
$aggregated_data[$client_id][$product_id]['clicks'] += (int) $row['clicks'];
|
||||
$aggregated_data[$client_id][$product_id]['cost'] += (float) $row['cost'];
|
||||
$aggregated_data[$client_id][$product_id]['conversions'] += (float) $row['conversions'];
|
||||
$aggregated_data[$client_id][$product_id]['conversions_value'] += (float) $row['conversions_value'];
|
||||
return 0;
|
||||
}
|
||||
|
||||
$products_ids = $mdb -> select( 'products', 'id', [ 'client_id' => (int) $client_id ] );
|
||||
$products_ids_array = [];
|
||||
foreach ( $products_ids as $product_id )
|
||||
$client_bestseller_min_roas = (int) \factory\Products::get_client_bestseller_min_roas( $client_id );
|
||||
|
||||
$rows = $mdb -> query(
|
||||
'SELECT
|
||||
p.id AS product_id,
|
||||
p.name,
|
||||
ph.campaign_id,
|
||||
ph.ad_group_id,
|
||||
COALESCE( SUM( ph.impressions ), 0 ) AS impressions,
|
||||
COALESCE( SUM( ph.clicks ), 0 ) AS clicks,
|
||||
COALESCE( SUM( ph.cost ), 0 ) AS cost,
|
||||
COALESCE( SUM( ph.conversions ), 0 ) AS conversions,
|
||||
COALESCE( SUM( ph.conversions_value ), 0 ) AS conversions_value
|
||||
FROM products AS p
|
||||
LEFT JOIN products_history AS ph ON p.id = ph.product_id
|
||||
WHERE p.client_id = :client_id
|
||||
GROUP BY p.id, p.name, ph.campaign_id, ph.ad_group_id',
|
||||
[ ':client_id' => $client_id ]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$product_ids = $mdb -> select( 'products', 'id', [ 'client_id' => $client_id ] );
|
||||
$product_ids = array_values( array_unique( array_map( 'intval', (array) $product_ids ) ) );
|
||||
|
||||
if ( !empty( $product_ids ) )
|
||||
{
|
||||
$products_ids_array[] = (int) $product_id;
|
||||
$mdb -> delete( 'products_temp', [ 'product_id' => $product_ids ] );
|
||||
}
|
||||
|
||||
if ( !empty( $products_ids_array ) )
|
||||
// products_data jest globalne per product_id, wiec klasyfikacje liczymy globalnie.
|
||||
$global_totals = $mdb -> query(
|
||||
'SELECT
|
||||
p.id AS product_id,
|
||||
COALESCE( SUM( ph.impressions ), 0 ) AS impressions,
|
||||
COALESCE( SUM( ph.clicks ), 0 ) AS clicks,
|
||||
COALESCE( SUM( ph.cost ), 0 ) AS cost,
|
||||
COALESCE( SUM( ph.conversions ), 0 ) AS conversions,
|
||||
COALESCE( SUM( ph.conversions_value ), 0 ) AS conversions_value
|
||||
FROM products AS p
|
||||
LEFT JOIN products_history AS ph ON p.id = ph.product_id
|
||||
WHERE p.client_id = :client_id
|
||||
GROUP BY p.id',
|
||||
[ ':client_id' => $client_id ]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
foreach ( $global_totals as $total )
|
||||
{
|
||||
$mdb -> delete( 'products_temp', [ 'product_id' => $products_ids_array ] );
|
||||
$product_id = (int) ( $total['product_id'] ?? 0 );
|
||||
if ( $product_id <= 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$total_cost = (float) ( $total['cost'] ?? 0 );
|
||||
$total_conversions = (float) ( $total['conversions'] ?? 0 );
|
||||
$total_conversion_value = (float) ( $total['conversions_value'] ?? 0 );
|
||||
$total_roas = ( $total_conversions > 0 && $total_cost > 0 ) ? round( $total_conversion_value / $total_cost, 2 ) * 100 : 0;
|
||||
|
||||
$custom_label_4 = \factory\Products::get_product_data( $product_id, 'custom_label_4' );
|
||||
if ( $custom_label_4 == null || ( $custom_label_4 == 'bestseller' && $client_bestseller_min_roas > 0 ) )
|
||||
{
|
||||
$new_custom_label_4 = ( $total_roas > $client_bestseller_min_roas && $total_conversions > 10 ) ? 'bestseller' : null;
|
||||
|
||||
$offers_data_tmp = $mdb -> get( 'products_data', '*', [ 'product_id' => $product_id ] );
|
||||
if ( isset( $offers_data_tmp['id'] ) )
|
||||
{
|
||||
if ( $new_custom_label_4 != $offers_data_tmp['custom_label_4'] )
|
||||
{
|
||||
$mdb -> insert( 'products_comments', [
|
||||
'product_id' => $product_id,
|
||||
'comment' => 'Zmiana pola "custom_label_4" na: ' . $new_custom_label_4,
|
||||
'type' => 1,
|
||||
'date_add' => date( 'Y-m-d' )
|
||||
] );
|
||||
}
|
||||
|
||||
$mdb -> update( 'products_data', [ 'custom_label_4' => $new_custom_label_4 ], [ 'id' => $offers_data_tmp['id'] ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$mdb -> insert( 'products_data', [
|
||||
'product_id' => $product_id,
|
||||
'custom_label_4' => $new_custom_label_4
|
||||
] );
|
||||
|
||||
if ( $new_custom_label_4 == 'bestseller' )
|
||||
{
|
||||
$mdb -> insert( 'products_comments', [
|
||||
'product_id' => $product_id,
|
||||
'comment' => 'Zmiana pola "custom_label_4" na: bestseller',
|
||||
'type' => 1,
|
||||
'date_add' => date( 'Y-m-d' )
|
||||
] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$processed_rows = 0;
|
||||
|
||||
foreach ( $aggregated_data as $client_offers )
|
||||
foreach ( $rows as $row )
|
||||
{
|
||||
foreach ( $client_offers as $offer_data )
|
||||
$product_id = (int) ( $row['product_id'] ?? 0 );
|
||||
if ( $product_id <= 0 )
|
||||
{
|
||||
// Obliczamy wartoci CPC oraz ROAS
|
||||
$cpc = $offer_data['clicks'] > 0 ? round( $offer_data['cost'] / $offer_data['clicks'], 6 ) : 0;
|
||||
$roas = ( $offer_data['conversions'] > 0 and $offer_data['cost'] ) ? round( $offer_data['conversions_value'] / $offer_data['cost'], 2 ) * 100 : 0;
|
||||
|
||||
$impressions_30 = \factory\Products::get_impressions_30( $offer_data['product_id'] );
|
||||
|
||||
// update custom_label_4 only current is empty or is bestseller
|
||||
$custom_label_4 = \factory\Products::get_product_data( $offer_data['product_id'], 'custom_label_4' );
|
||||
if ( $custom_label_4 == null || ( $custom_label_4 == 'bestseller' and (int)$client_bestseller_min_roas > 0 ) )
|
||||
{
|
||||
if ( $roas > $client_bestseller_min_roas and $offer_data['conversions'] > 10 )
|
||||
{
|
||||
$new_custom_label_4 = 'bestseller';
|
||||
}
|
||||
else
|
||||
{
|
||||
$new_custom_label_4 = null;
|
||||
}
|
||||
|
||||
$offers_data_tmp = $mdb -> get( 'products_data', '*', [ 'product_id' => $offer_data['product_id'] ] );
|
||||
if ( isset( $offers_data_tmp['id'] ) )
|
||||
{
|
||||
if ( $new_custom_label_4 != $offers_data_tmp['custom_label_4'] )
|
||||
$mdb -> insert( 'products_comments', [
|
||||
'product_id' => $offer_data['product_id'],
|
||||
'comment' => 'Zmiana pola "custom_label_4" na: ' . $new_custom_label_4,
|
||||
'type' => 1,
|
||||
'date_add' => date( 'Y-m-d' )
|
||||
] );
|
||||
|
||||
$mdb -> update( 'products_data', [
|
||||
'custom_label_4' => $new_custom_label_4
|
||||
], [ 'id' => $offers_data_tmp['id'] ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$mdb -> insert( 'products_data', [
|
||||
'product_id' => $offer_data['product_id'],
|
||||
'custom_label_4' => $new_custom_label_4
|
||||
] );
|
||||
|
||||
if ( $new_custom_label_4 == 'bestseller' )
|
||||
{
|
||||
$mdb -> insert( 'products_comments', [
|
||||
'product_id' => $offer_data['product_id'],
|
||||
'comment' => 'Zmiana pola "custom_label_4" na: bestseller',
|
||||
'type' => 1,
|
||||
'date_add' => date( 'Y-m-d' )
|
||||
] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$clicks_30 = \factory\Products::get_clicks_30( $offer_data['product_id'] );
|
||||
|
||||
$mdb -> insert( 'products_temp', [
|
||||
'product_id' => $offer_data['product_id'],
|
||||
'name' => $offer_data['name'],
|
||||
'impressions' => $offer_data['impressions'],
|
||||
'impressions_30' => $impressions_30,
|
||||
'clicks' => $offer_data['clicks'],
|
||||
'clicks_30' => $clicks_30,
|
||||
'ctr' => ( $offer_data['impressions'] > 0 ) ? round( $offer_data['clicks'] / $offer_data['impressions'], 4 ) * 100 : 0,
|
||||
'cost' => $offer_data['cost'],
|
||||
'conversions' => $offer_data['conversions'],
|
||||
'conversions_value' => $offer_data['conversions_value'],
|
||||
'cpc' => $cpc,
|
||||
'roas' => $roas,
|
||||
] );
|
||||
|
||||
$processed_rows++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$campaign_id = (int) ( $row['campaign_id'] ?? 0 );
|
||||
$ad_group_id = (int) ( $row['ad_group_id'] ?? 0 );
|
||||
$impressions = (int) ( $row['impressions'] ?? 0 );
|
||||
$clicks = (int) ( $row['clicks'] ?? 0 );
|
||||
$cost = (float) ( $row['cost'] ?? 0 );
|
||||
$conversions = (float) ( $row['conversions'] ?? 0 );
|
||||
$conversions_value = (float) ( $row['conversions_value'] ?? 0 );
|
||||
|
||||
// Pomijamy puste scope bez danych.
|
||||
if ( $impressions <= 0 && $clicks <= 0 && $cost <= 0 && $conversions <= 0 && $conversions_value <= 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$cpc = $clicks > 0 ? round( $cost / $clicks, 6 ) : 0;
|
||||
$roas = ( $conversions > 0 && $cost > 0 ) ? round( $conversions_value / $cost, 2 ) * 100 : 0;
|
||||
$impressions_30 = (int) \factory\Products::get_impressions_30( $product_id, $campaign_id, $ad_group_id );
|
||||
$clicks_30 = (int) \factory\Products::get_clicks_30( $product_id, $campaign_id, $ad_group_id );
|
||||
|
||||
$mdb -> insert( 'products_temp', [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_group_id' => $ad_group_id,
|
||||
'name' => $row['name'],
|
||||
'impressions' => $impressions,
|
||||
'impressions_30' => $impressions_30,
|
||||
'clicks' => $clicks,
|
||||
'clicks_30' => $clicks_30,
|
||||
'ctr' => ( $impressions > 0 ) ? round( $clicks / $impressions, 4 ) * 100 : 0,
|
||||
'cost' => $cost,
|
||||
'conversions' => $conversions,
|
||||
'conversions_value' => $conversions_value,
|
||||
'cpc' => $cpc,
|
||||
'roas' => $roas,
|
||||
] );
|
||||
|
||||
$processed_rows++;
|
||||
}
|
||||
|
||||
return $processed_rows;
|
||||
@@ -537,11 +772,20 @@ class Cron
|
||||
$products = $mdb -> select( 'products', 'id', [ 'client_id' => $client_id ] );
|
||||
foreach ( $products as $product )
|
||||
{
|
||||
$dates = $mdb -> query( 'SELECT id, date_add FROM products_history WHERE product_id = ' . $product . ' AND updated = 1 ORDER BY date_add DESC' ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
foreach ( $dates as $date )
|
||||
$scopes = $mdb -> query( 'SELECT DISTINCT campaign_id, ad_group_id, date_add FROM products_history WHERE product_id = ' . $product . ' AND updated = 1 ORDER BY date_add DESC' ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
foreach ( $scopes as $scope )
|
||||
{
|
||||
self::cron_product_history_30_save( $product, $date['date_add'] );
|
||||
$mdb -> update( 'products_history', [ 'updated' => 0 ], [ 'id' => $date['id'] ] );
|
||||
$campaign_id = (int) ( $scope['campaign_id'] ?? 0 );
|
||||
$ad_group_id = (int) ( $scope['ad_group_id'] ?? 0 );
|
||||
$date_add = $scope['date_add'] ?? '';
|
||||
|
||||
self::cron_product_history_30_save( $product, $date_add, $campaign_id, $ad_group_id );
|
||||
$mdb -> update( 'products_history', [ 'updated' => 0 ], [ 'AND' => [
|
||||
'product_id' => $product,
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_group_id' => $ad_group_id,
|
||||
'date_add' => $date_add
|
||||
] ] );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,19 +795,61 @@ class Cron
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function get_roas_all_time( $product_id, $date_to )
|
||||
static public function get_roas_all_time( $product_id, $date_to, $campaign_id = 0, $ad_group_id = 0 )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$roas_all_time = $mdb -> query( 'SELECT SUM(conversions_value) / SUM(cost) * 100 AS roas_all_time FROM products_history WHERE product_id = ' . $product_id . ' AND date_add <= \'' . $date_to . '\'' ) -> fetchColumn();
|
||||
$product_id = (int) $product_id;
|
||||
$campaign_id = (int) $campaign_id;
|
||||
$ad_group_id = (int) $ad_group_id;
|
||||
|
||||
$sql = 'SELECT SUM(conversions_value) / SUM(cost) * 100 AS roas_all_time
|
||||
FROM products_history
|
||||
WHERE product_id = :product_id
|
||||
AND date_add <= :date_to
|
||||
AND campaign_id = :campaign_id
|
||||
AND ad_group_id = :ad_group_id';
|
||||
|
||||
$roas_all_time = $mdb -> query( $sql, [
|
||||
':product_id' => $product_id,
|
||||
':date_to' => $date_to,
|
||||
':campaign_id' => $campaign_id,
|
||||
':ad_group_id' => $ad_group_id
|
||||
] ) -> fetchColumn();
|
||||
return round( $roas_all_time, 2 );
|
||||
}
|
||||
|
||||
static public function cron_product_history_30_save( $product_id, $date_to )
|
||||
static public function cron_product_history_30_save( $product_id, $date_to, $campaign_id = 0, $ad_group_id = 0 )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$data = $mdb -> query( 'SELECT * FROM products_history WHERE product_id = ' . $product_id . ' AND date_add <= \'' . $date_to . '\' ORDER BY date_add DESC LIMIT 30' ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
$product_id = (int) $product_id;
|
||||
$campaign_id = (int) $campaign_id;
|
||||
$ad_group_id = (int) $ad_group_id;
|
||||
|
||||
$data = $mdb -> query(
|
||||
'SELECT
|
||||
date_add,
|
||||
SUM( impressions ) AS impressions,
|
||||
SUM( clicks ) AS clicks,
|
||||
SUM( cost ) AS cost,
|
||||
SUM( conversions ) AS conversions,
|
||||
SUM( conversions_value ) AS conversions_value
|
||||
FROM products_history
|
||||
WHERE product_id = :product_id
|
||||
AND campaign_id = :campaign_id
|
||||
AND ad_group_id = :ad_group_id
|
||||
AND date_add <= :date_to
|
||||
GROUP BY date_add
|
||||
ORDER BY date_add DESC
|
||||
LIMIT 30',
|
||||
[
|
||||
':product_id' => $product_id,
|
||||
':campaign_id' => $campaign_id,
|
||||
':ad_group_id' => $ad_group_id,
|
||||
':date_to' => $date_to
|
||||
]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
// Inicjalizacja tablic do przechowywania danych
|
||||
$offers_data = [];
|
||||
@@ -605,9 +891,29 @@ class Cron
|
||||
$conversions_value = $offer['conversions_value'];
|
||||
$roas = ( $conversions_value > 0 and $cost ) ? round( $conversions_value / $cost, 2 ) * 100 : 0;
|
||||
|
||||
if ( $mdb -> count( 'products_history', [ 'AND' => [ 'product_id' => $product_id, 'date_add[<=]' => $date_to ] ] ) >= 14 )
|
||||
$days_count_for_product = (int) $mdb -> query(
|
||||
'SELECT COUNT( DISTINCT date_add )
|
||||
FROM products_history
|
||||
WHERE product_id = :product_id
|
||||
AND campaign_id = :campaign_id
|
||||
AND ad_group_id = :ad_group_id
|
||||
AND date_add <= :date_to',
|
||||
[
|
||||
':product_id' => $product_id,
|
||||
':campaign_id' => $campaign_id,
|
||||
':ad_group_id' => $ad_group_id,
|
||||
':date_to' => $date_to
|
||||
]
|
||||
) -> fetchColumn();
|
||||
|
||||
if ( $days_count_for_product >= 14 )
|
||||
{
|
||||
if ( $mdb -> count( 'products_history_30', [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date_to ] ] ) > 0 )
|
||||
if ( $mdb -> count( 'products_history_30', [ 'AND' => [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_group_id' => $ad_group_id,
|
||||
'date_add' => $date_to
|
||||
] ] ) > 0 )
|
||||
{
|
||||
$mdb -> update( 'products_history_30', [
|
||||
'impressions' => $impressions,
|
||||
@@ -617,13 +923,20 @@ class Cron
|
||||
'conversions' => $conversions,
|
||||
'conversions_value' => $conversions_value,
|
||||
'roas' => $roas,
|
||||
'roas_all_time' => self::get_roas_all_time( $product_id, $date_to )
|
||||
], [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date_to ] ] );
|
||||
'roas_all_time' => self::get_roas_all_time( $product_id, $date_to, $campaign_id, $ad_group_id )
|
||||
], [ 'AND' => [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_group_id' => $ad_group_id,
|
||||
'date_add' => $date_to
|
||||
] ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$mdb -> insert( 'products_history_30', [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_group_id' => $ad_group_id,
|
||||
'impressions' => $impressions,
|
||||
'clicks' => $clicks,
|
||||
'ctr' => $ctr,
|
||||
@@ -631,7 +944,7 @@ class Cron
|
||||
'conversions' => $conversions,
|
||||
'conversions_value' => $conversions_value,
|
||||
'roas' => $roas,
|
||||
'roas_all_time' => self::get_roas_all_time( $product_id, $date_to ),
|
||||
'roas_all_time' => self::get_roas_all_time( $product_id, $date_to, $campaign_id, $ad_group_id ),
|
||||
'date_add' => $date_to
|
||||
] );
|
||||
}
|
||||
@@ -1032,6 +1345,8 @@ class Cron
|
||||
continue;
|
||||
}
|
||||
|
||||
$advertising_channel_type = strtoupper( trim( (string) ( $campaign['advertising_channel_type'] ?? '' ) ) );
|
||||
|
||||
$account_30_totals['budget'] += (float) ( $campaign['budget'] ?? 0 );
|
||||
$account_30_totals['money_spent'] += (float) ( $campaign['money_spent'] ?? 0 );
|
||||
$account_30_totals['conversion_value'] += (float) ( $campaign['conversion_value'] ?? 0 );
|
||||
@@ -1044,7 +1359,8 @@ class Cron
|
||||
$mdb -> insert( 'campaigns', [
|
||||
'client_id' => $client['id'],
|
||||
'campaign_id' => $external_campaign_id,
|
||||
'campaign_name' => $campaign['campaign_name']
|
||||
'campaign_name' => $campaign['campaign_name'],
|
||||
'advertising_channel_type' => $advertising_channel_type !== '' ? $advertising_channel_type : null
|
||||
] );
|
||||
$db_campaign_id = $mdb -> id();
|
||||
}
|
||||
@@ -1056,7 +1372,8 @@ class Cron
|
||||
] ] );
|
||||
|
||||
$mdb -> update( 'campaigns', [
|
||||
'campaign_name' => $campaign['campaign_name']
|
||||
'campaign_name' => $campaign['campaign_name'],
|
||||
'advertising_channel_type' => $advertising_channel_type !== '' ? $advertising_channel_type : null
|
||||
], [ 'id' => $db_campaign_id ] );
|
||||
}
|
||||
|
||||
@@ -1111,7 +1428,8 @@ class Cron
|
||||
$mdb -> insert( 'campaigns', [
|
||||
'client_id' => $client['id'],
|
||||
'campaign_id' => 0,
|
||||
'campaign_name' => '--- konto ---'
|
||||
'campaign_name' => '--- konto ---',
|
||||
'advertising_channel_type' => null
|
||||
] );
|
||||
$db_account_campaign_id = $mdb -> id();
|
||||
}
|
||||
@@ -1123,7 +1441,8 @@ class Cron
|
||||
] ] );
|
||||
|
||||
$mdb -> update( 'campaigns', [
|
||||
'campaign_name' => '--- konto ---'
|
||||
'campaign_name' => '--- konto ---',
|
||||
'advertising_channel_type' => null
|
||||
], [ 'id' => $db_account_campaign_id ] );
|
||||
}
|
||||
|
||||
@@ -1358,6 +1677,15 @@ class Cron
|
||||
$db_campaign_id = (int) ( $campaigns_db_map[ $campaign_external_id ] ?? 0 );
|
||||
$db_ad_group_id = (int) ( $ad_group_db_map[ $campaign_external_id . '|' . $ad_group_external_id ] ?? 0 );
|
||||
|
||||
if ( $db_campaign_id > 0 && $db_ad_group_id <= 0 && $ad_group_external_id === '0' )
|
||||
{
|
||||
$db_ad_group_id = self::ensure_campaign_level_ad_group( $db_campaign_id, $date_sync );
|
||||
if ( $db_ad_group_id > 0 )
|
||||
{
|
||||
$ad_group_db_map[ $campaign_external_id . '|0' ] = $db_ad_group_id;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $db_campaign_id <= 0 || $db_ad_group_id <= 0 )
|
||||
{
|
||||
continue;
|
||||
@@ -1404,6 +1732,50 @@ class Cron
|
||||
return [ 'count' => $count, 'errors' => [] ];
|
||||
}
|
||||
|
||||
static private function ensure_campaign_level_ad_group( $db_campaign_id, $date_sync )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$db_campaign_id = (int) $db_campaign_id;
|
||||
if ( $db_campaign_id <= 0 )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
$existing_id = (int) $mdb -> get( 'campaign_ad_groups', 'id', [
|
||||
'AND' => [
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => 0
|
||||
]
|
||||
] );
|
||||
|
||||
if ( $existing_id > 0 )
|
||||
{
|
||||
return $existing_id;
|
||||
}
|
||||
|
||||
$mdb -> insert( 'campaign_ad_groups', [
|
||||
'campaign_id' => $db_campaign_id,
|
||||
'ad_group_id' => 0,
|
||||
'ad_group_name' => 'PMax (bez grup reklam)',
|
||||
'impressions_30' => 0,
|
||||
'clicks_30' => 0,
|
||||
'cost_30' => 0,
|
||||
'conversions_30' => 0,
|
||||
'conversion_value_30' => 0,
|
||||
'roas_30' => 0,
|
||||
'impressions_all_time' => 0,
|
||||
'clicks_all_time' => 0,
|
||||
'cost_all_time' => 0,
|
||||
'conversions_all_time' => 0,
|
||||
'conversion_value_all_time' => 0,
|
||||
'roas_all_time' => 0,
|
||||
'date_sync' => $date_sync
|
||||
] );
|
||||
|
||||
return (int) $mdb -> id();
|
||||
}
|
||||
|
||||
static private function sync_campaign_negative_keywords_for_client( $campaigns_db_map, $ad_group_db_map, $customer_id, $api, $date_sync )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
@@ -36,6 +36,27 @@ class Products
|
||||
] );
|
||||
}
|
||||
|
||||
static public function get_campaigns_list()
|
||||
{
|
||||
$client_id = (int) \S::get( 'client_id' );
|
||||
echo json_encode( [ 'campaigns' => \factory\Campaigns::get_campaigns_list( $client_id, true ) ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function get_campaign_ad_groups()
|
||||
{
|
||||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||||
|
||||
if ( $campaign_id <= 0 )
|
||||
{
|
||||
echo json_encode( [ 'ad_groups' => [] ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
echo json_encode( [ 'ad_groups' => \factory\Campaigns::get_campaign_ad_groups( $campaign_id ) ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function comment_add()
|
||||
{
|
||||
$product_id = \S::get( 'product_id' );
|
||||
@@ -178,6 +199,8 @@ class Products
|
||||
static public function get_products()
|
||||
{
|
||||
$client_id = \S::get( 'client_id' );
|
||||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||||
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||||
$limit = \S::get( 'length' ) ? \S::get( 'length' ) : 10;
|
||||
$start = \S::get( 'start' ) ? \S::get( 'start' ) : 0;
|
||||
|
||||
@@ -186,7 +209,7 @@ class Products
|
||||
$search = $_POST['search']['value'];
|
||||
|
||||
// ➊ MIN/MAX ROAS dla kontekstu klienta (opcjonalnie z filtrem search)
|
||||
$bounds = \factory\Products::get_roas_bounds( $client_id, $search );
|
||||
$bounds = \factory\Products::get_roas_bounds( (int) $client_id, $search, $campaign_id, $ad_group_id );
|
||||
$roas_min = (float)$bounds['min'];
|
||||
$roas_max = (float)$bounds['max'];
|
||||
// zabezpieczenie przed dzieleniem przez 0
|
||||
@@ -221,12 +244,13 @@ class Products
|
||||
</div>';
|
||||
};
|
||||
|
||||
$db_results = \factory\Products::get_products( $client_id, $search, $limit, $start, $order_name, $order_dir );
|
||||
$recordsTotal = \factory\Products::get_records_total_products( $client_id, $search );
|
||||
$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 );
|
||||
|
||||
$data['draw'] = \S::get( 'draw' );
|
||||
$data['recordsTotal'] = $recordsTotal;
|
||||
$data['recordsFiltered'] = $recordsTotal;
|
||||
$data['data'] = [];
|
||||
|
||||
foreach ( $db_results as $row )
|
||||
{
|
||||
@@ -270,8 +294,10 @@ class Products
|
||||
'', // checkbox column
|
||||
$row['product_id'],
|
||||
$row['offer_id'],
|
||||
htmlspecialchars( (string) ( $row['campaign_name'] ?? '' ) ),
|
||||
htmlspecialchars( (string) ( $row['ad_group_name'] ?? '' ) ),
|
||||
'<div class="table-product-title" product_id="' . $row['product_id'] . '">
|
||||
<a href="/products/product_history/client_id=' . $client_id . '&product_id=' . $row['product_id'] . '" target="_blank" class="' . $custom_class . '">
|
||||
<a href="/products/product_history/client_id=' . $client_id . '&product_id=' . $row['product_id'] . '&campaign_id=' . (int) ( $row['campaign_id'] ?? 0 ) . '&ad_group_id=' . (int) ( $row['ad_group_id'] ?? 0 ) . '" target="_blank" class="' . $custom_class . '">
|
||||
' . $row['name'] . '
|
||||
</a>
|
||||
<span class="edit-product-title" product_id="' . $row['product_id'] . '">
|
||||
@@ -357,10 +383,14 @@ class Products
|
||||
{
|
||||
$client_id = \S::get( 'client_id' );
|
||||
$product_id = \S::get( 'product_id' );
|
||||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||||
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||||
|
||||
return \Tpl::view( 'products/product_history', [
|
||||
'client_id' => $client_id,
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_group_id' => $ad_group_id,
|
||||
'min_roas' => \factory\Products::get_min_roas( $product_id )
|
||||
] );
|
||||
}
|
||||
@@ -369,15 +399,18 @@ class Products
|
||||
{
|
||||
$client_id= \S::get( 'client_id' );
|
||||
$product_id = \S::get( 'product_id' );
|
||||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||||
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||||
$start = \S::get( 'start' ) ? \S::get( 'start' ) : 0;
|
||||
$limit = \S::get( 'length' ) ? \S::get( 'length' ) : 10;
|
||||
|
||||
$db_results = \factory\Products::get_product_history( $client_id, $product_id, $start, $limit );
|
||||
$recordsTotal = \factory\Products::get_records_total_product_history( $client_id, $product_id );
|
||||
$db_results = \factory\Products::get_product_history( $client_id, $product_id, $start, $limit, $campaign_id, $ad_group_id );
|
||||
$recordsTotal = \factory\Products::get_records_total_product_history( $client_id, $product_id, $campaign_id, $ad_group_id );
|
||||
|
||||
$data['draw'] = \S::get( 'draw' );
|
||||
$data['recordsTotal'] = $recordsTotal;
|
||||
$data['recordsFiltered'] = $recordsTotal;
|
||||
$data['data'] = [];
|
||||
|
||||
foreach ( $db_results as $row )
|
||||
{
|
||||
@@ -416,13 +449,16 @@ class Products
|
||||
{
|
||||
$client_id = \S::get( 'client_id' );
|
||||
$product_id = \S::get( 'product_id' );
|
||||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||||
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||||
$limit = \S::get( 'length' ) ? \S::get( 'length' ) : 360;
|
||||
$start = \S::get( 'start' ) ? \S::get( 'start' ) : 0;
|
||||
|
||||
$db_results = \factory\Products::get_product_history_30( $client_id, $product_id, $start, $limit );
|
||||
$db_results = \factory\Products::get_product_history_30( $client_id, $product_id, $start, $limit, $campaign_id, $ad_group_id );
|
||||
|
||||
$impressions = [];
|
||||
$clicks = [];
|
||||
$ctr = [];
|
||||
$cost = [];
|
||||
$conversions = [];
|
||||
$conversions_value = [];
|
||||
@@ -507,4 +543,4 @@ class Products
|
||||
echo json_encode( [ 'status' => 'ok' ] );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,45 @@ class Campaigns
|
||||
return $mdb -> select( 'clients', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
|
||||
}
|
||||
|
||||
static public function get_campaigns_list( $client_id )
|
||||
static public function get_campaigns_list( $client_id, $only_active = false )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> select( 'campaigns', '*', [ 'client_id' => $client_id, 'ORDER' => [ 'campaign_name' => 'ASC' ] ] );
|
||||
|
||||
$client_id = (int) $client_id;
|
||||
|
||||
if ( !$only_active )
|
||||
{
|
||||
return $mdb -> select( 'campaigns', '*', [ 'client_id' => $client_id, 'ORDER' => [ 'campaign_name' => 'ASC' ] ] );
|
||||
}
|
||||
|
||||
$latest_date = $mdb -> query(
|
||||
'SELECT MAX( ch.date_add )
|
||||
FROM campaigns_history AS ch
|
||||
INNER JOIN campaigns AS c ON c.id = ch.campaign_id
|
||||
WHERE c.client_id = :client_id
|
||||
AND c.campaign_id <> 0',
|
||||
[ ':client_id' => $client_id ]
|
||||
) -> fetchColumn();
|
||||
|
||||
if ( !$latest_date )
|
||||
{
|
||||
return $mdb -> select( 'campaigns', '*', [ 'client_id' => $client_id, 'ORDER' => [ 'campaign_name' => 'ASC' ] ] );
|
||||
}
|
||||
|
||||
return $mdb -> query(
|
||||
'SELECT c.*
|
||||
FROM campaigns AS c
|
||||
LEFT JOIN campaigns_history AS ch
|
||||
ON ch.campaign_id = c.id
|
||||
AND ch.date_add = :latest_date
|
||||
WHERE c.client_id = :client_id
|
||||
AND ( c.campaign_id = 0 OR ch.id IS NOT NULL )
|
||||
ORDER BY c.campaign_name ASC',
|
||||
[
|
||||
':client_id' => $client_id,
|
||||
':latest_date' => $latest_date
|
||||
]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function get_campaign_history_data( $campaign_id, $start, $length, $revert = false )
|
||||
@@ -144,6 +179,7 @@ class Campaigns
|
||||
st.ad_group_id AS db_ad_group_id,
|
||||
c.client_id,
|
||||
c.campaign_id AS external_campaign_id,
|
||||
c.advertising_channel_type,
|
||||
ag.ad_group_id AS external_ad_group_id,
|
||||
cl.google_ads_customer_id
|
||||
FROM campaign_search_terms AS st
|
||||
|
||||
@@ -61,63 +61,181 @@ class Products
|
||||
return $mdb -> update( 'products', [ 'min_roas' => $min_roas ], [ 'id' => $product_id ] );
|
||||
}
|
||||
|
||||
static public function get_products( $client_id, $search, $limit, $start, $order_name, $order_dir )
|
||||
static private function build_scope_filters( &$sql, &$params, $campaign_id, $ad_group_id )
|
||||
{
|
||||
global $mdb;
|
||||
$campaign_id = (int) $campaign_id;
|
||||
$ad_group_id = (int) $ad_group_id;
|
||||
|
||||
if ( $search )
|
||||
return $mdb -> query( 'SELECT pt.*, p.offer_id, p.min_roas FROM products_temp AS pt INNER JOIN products AS p ON p.id = pt.product_id WHERE client_id = \'' . $client_id . '\' AND ( pt.name LIKE \'%' . $search . '%\' OR offer_id LIKE \'%' . $search . '%\' ) ORDER BY ' . $order_name . ' ' . $order_dir . ', id DESC LIMIT ' . $start . ', ' . $limit ) -> fetchAll();
|
||||
else
|
||||
return $mdb -> query( 'SELECT pt.*, p.offer_id, p.min_roas FROM products_temp AS pt INNER JOIN products AS p ON p.id = pt.product_id WHERE client_id = \'' . $client_id . '\' ORDER BY ' . $order_name . ' ' . $order_dir . ', id DESC LIMIT ' . $start . ', ' . $limit ) -> fetchAll();
|
||||
if ( $campaign_id > 0 )
|
||||
{
|
||||
$sql .= ' AND pt.campaign_id = :campaign_id';
|
||||
$params[':campaign_id'] = $campaign_id;
|
||||
}
|
||||
|
||||
if ( $ad_group_id > 0 )
|
||||
{
|
||||
$sql .= ' AND pt.ad_group_id = :ad_group_id';
|
||||
$params[':ad_group_id'] = $ad_group_id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// \factory\Products.php
|
||||
public static function get_roas_bounds(int $client_id, ?string $search = null): array
|
||||
static public function get_products( $client_id, $search, $limit, $start, $order_name, $order_dir, $campaign_id = 0, $ad_group_id = 0 )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$params = [':client_id' => $client_id];
|
||||
$limit = max( 1, (int) $limit );
|
||||
$start = max( 0, (int) $start );
|
||||
$order_dir = strtoupper( (string) $order_dir ) === 'ASC' ? 'ASC' : 'DESC';
|
||||
|
||||
$sql = 'SELECT MIN(p.min_roas) AS min_roas, MAX(pt.roas) AS max_roas
|
||||
$order_map = [
|
||||
'offer_id' => 'p.offer_id',
|
||||
'campaign_name' => 'c.campaign_name',
|
||||
'ad_group_name' => 'ag.ad_group_name',
|
||||
'name' => 'pt.name',
|
||||
'impressions' => 'pt.impressions',
|
||||
'impressions_30' => 'pt.impressions_30',
|
||||
'clicks' => 'pt.clicks',
|
||||
'clicks_30' => 'pt.clicks_30',
|
||||
'ctr' => 'pt.ctr',
|
||||
'cost' => 'pt.cost',
|
||||
'cpc' => 'pt.cpc',
|
||||
'conversions' => 'pt.conversions',
|
||||
'conversions_value' => 'pt.conversions_value',
|
||||
'roas' => 'pt.roas',
|
||||
'min_roas' => 'p.min_roas'
|
||||
];
|
||||
|
||||
$order_sql = $order_map[ $order_name ] ?? 'pt.clicks';
|
||||
|
||||
$params = [ ':client_id' => (int) $client_id ];
|
||||
$sql = 'SELECT pt.*, p.offer_id, p.min_roas,
|
||||
COALESCE( c.campaign_name, \'--- brak kampanii ---\' ) AS campaign_name,
|
||||
CASE
|
||||
WHEN pt.ad_group_id = 0 THEN \'PMax (bez grup reklam)\'
|
||||
ELSE COALESCE( ag.ad_group_name, \'--- brak grupy reklam ---\' )
|
||||
END AS ad_group_name
|
||||
FROM products_temp AS pt
|
||||
INNER JOIN products AS p ON p.id = pt.product_id
|
||||
WHERE p.client_id = :client_id AND conversions > 10';
|
||||
LEFT JOIN campaigns AS c ON c.id = pt.campaign_id
|
||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id
|
||||
WHERE p.client_id = :client_id';
|
||||
|
||||
if ($search) {
|
||||
$sql .= ' AND (pt.name LIKE :search OR p.offer_id LIKE :search)';
|
||||
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
||||
|
||||
if ( $search )
|
||||
{
|
||||
$sql .= ' AND (
|
||||
pt.name LIKE :search
|
||||
OR p.offer_id LIKE :search
|
||||
OR c.campaign_name LIKE :search
|
||||
OR ag.ad_group_name LIKE :search
|
||||
)';
|
||||
$params[':search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
$row = $mdb->query($sql, $params)->fetch(\PDO::FETCH_ASSOC);
|
||||
$sql .= ' ORDER BY ' . $order_sql . ' ' . $order_dir . ', pt.id DESC LIMIT ' . $start . ', ' . $limit;
|
||||
|
||||
return [
|
||||
'min' => isset($row['min_roas']) ? (float)$row['min_roas'] : 0.0,
|
||||
'max' => isset($row['max_roas']) ? (float)$row['max_roas'] : 0.0,
|
||||
];
|
||||
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function get_records_total_products( $client_id, $search )
|
||||
public static function get_roas_bounds( int $client_id, ?string $search = null, int $campaign_id = 0, int $ad_group_id = 0 ): array
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$params = [ ':client_id' => $client_id ];
|
||||
|
||||
$sql = 'SELECT MIN( p.min_roas ) AS min_roas, MAX( pt.roas ) AS max_roas
|
||||
FROM products_temp AS pt
|
||||
INNER JOIN products AS p ON p.id = pt.product_id
|
||||
LEFT JOIN campaigns AS c ON c.id = pt.campaign_id
|
||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id
|
||||
WHERE p.client_id = :client_id
|
||||
AND pt.conversions > 10';
|
||||
|
||||
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
||||
|
||||
if ( $search )
|
||||
return $mdb -> query( 'SELECT COUNT(0) FROM products_temp AS pt INNER JOIN products AS p ON p.id = pt.product_id WHERE client_id = \'' . $client_id . '\' AND ( pt.name LIKE \'%' . $search . '%\' OR offer_id LIKE \'%' . $search . '%\' )' ) -> fetchColumn();
|
||||
else
|
||||
return $mdb -> query( 'SELECT COUNT(0) FROM products_temp AS pt INNER JOIN products AS p ON p.id = pt.product_id WHERE client_id = \'' . $client_id . '\'' ) -> fetchColumn();
|
||||
{
|
||||
$sql .= ' AND (
|
||||
pt.name LIKE :search
|
||||
OR p.offer_id LIKE :search
|
||||
OR c.campaign_name LIKE :search
|
||||
OR ag.ad_group_name LIKE :search
|
||||
)';
|
||||
$params[':search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
$row = $mdb -> query( $sql, $params ) -> fetch( \PDO::FETCH_ASSOC );
|
||||
|
||||
return [
|
||||
'min' => isset( $row['min_roas'] ) ? (float) $row['min_roas'] : 0.0,
|
||||
'max' => isset( $row['max_roas'] ) ? (float) $row['max_roas'] : 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
static public function get_records_total_products( $client_id, $search, $campaign_id = 0, $ad_group_id = 0 )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$params = [ ':client_id' => (int) $client_id ];
|
||||
$sql = 'SELECT COUNT(0)
|
||||
FROM products_temp AS pt
|
||||
INNER JOIN products AS p ON p.id = pt.product_id
|
||||
LEFT JOIN campaigns AS c ON c.id = pt.campaign_id
|
||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id
|
||||
WHERE p.client_id = :client_id';
|
||||
|
||||
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
||||
|
||||
if ( $search )
|
||||
{
|
||||
$sql .= ' AND (
|
||||
pt.name LIKE :search
|
||||
OR p.offer_id LIKE :search
|
||||
OR c.campaign_name LIKE :search
|
||||
OR ag.ad_group_name LIKE :search
|
||||
)';
|
||||
$params[':search'] = '%' . $search . '%';
|
||||
}
|
||||
|
||||
return $mdb -> query( $sql, $params ) -> fetchColumn();
|
||||
}
|
||||
|
||||
static public function get_product_full_context( $product_id )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> query(
|
||||
'SELECT p.id, p.offer_id, p.name, p.min_roas,
|
||||
pt.impressions, pt.impressions_30, pt.clicks, pt.clicks_30,
|
||||
pt.ctr, pt.cost, pt.cpc, pt.conversions, pt.conversions_value, pt.roas
|
||||
'SELECT
|
||||
p.id,
|
||||
p.offer_id,
|
||||
p.name,
|
||||
p.min_roas,
|
||||
COALESCE( SUM( pt.impressions ), 0 ) AS impressions,
|
||||
COALESCE( SUM( pt.impressions_30 ), 0 ) AS impressions_30,
|
||||
COALESCE( SUM( pt.clicks ), 0 ) AS clicks,
|
||||
COALESCE( SUM( pt.clicks_30 ), 0 ) AS clicks_30,
|
||||
CASE
|
||||
WHEN COALESCE( SUM( pt.impressions ), 0 ) > 0
|
||||
THEN ROUND( COALESCE( SUM( pt.clicks ), 0 ) / COALESCE( SUM( pt.impressions ), 0 ) * 100, 2 )
|
||||
ELSE 0
|
||||
END AS ctr,
|
||||
COALESCE( SUM( pt.cost ), 0 ) AS cost,
|
||||
CASE
|
||||
WHEN COALESCE( SUM( pt.clicks ), 0 ) > 0
|
||||
THEN ROUND( COALESCE( SUM( pt.cost ), 0 ) / COALESCE( SUM( pt.clicks ), 0 ), 6 )
|
||||
ELSE 0
|
||||
END AS cpc,
|
||||
COALESCE( SUM( pt.conversions ), 0 ) AS conversions,
|
||||
COALESCE( SUM( pt.conversions_value ), 0 ) AS conversions_value,
|
||||
CASE
|
||||
WHEN COALESCE( SUM( pt.cost ), 0 ) > 0
|
||||
THEN ROUND( COALESCE( SUM( pt.conversions_value ), 0 ) / COALESCE( SUM( pt.cost ), 0 ) * 100, 2 )
|
||||
ELSE 0
|
||||
END AS roas
|
||||
FROM products AS p
|
||||
LEFT JOIN products_temp AS pt ON pt.product_id = p.id
|
||||
WHERE p.id = :pid',
|
||||
WHERE p.id = :pid
|
||||
GROUP BY p.id, p.offer_id, p.name, p.min_roas',
|
||||
[ ':pid' => $product_id ]
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
@@ -139,34 +257,139 @@ class Products
|
||||
return $result;
|
||||
}
|
||||
|
||||
static public function get_product_history( $client_id, $product_id, $start, $limit )
|
||||
static public function get_product_history( $client_id, $product_id, $start, $limit, $campaign_id = 0, $ad_group_id = 0 )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> query( 'SELECT * FROM products_history AS ph WHERE ph.product_id = \'' . $product_id . '\' ORDER BY ph.date_add DESC LIMIT ' . $start . ', ' . $limit ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
$limit = max( 1, (int) $limit );
|
||||
$start = max( 0, (int) $start );
|
||||
|
||||
return $mdb -> query(
|
||||
'SELECT
|
||||
MAX( ph.id ) AS id,
|
||||
SUM( ph.impressions ) AS impressions,
|
||||
SUM( ph.clicks ) AS clicks,
|
||||
CASE WHEN SUM( ph.impressions ) > 0 THEN ROUND( SUM( ph.clicks ) / SUM( ph.impressions ) * 100, 2 ) ELSE 0 END AS ctr,
|
||||
SUM( ph.cost ) AS cost,
|
||||
SUM( ph.conversions ) AS conversions,
|
||||
SUM( ph.conversions_value ) AS conversions_value,
|
||||
ph.date_add
|
||||
FROM products_history AS ph
|
||||
INNER JOIN products AS p ON p.id = ph.product_id
|
||||
WHERE ph.product_id = :product_id
|
||||
AND p.client_id = :client_id
|
||||
AND ph.campaign_id = :campaign_id
|
||||
AND ph.ad_group_id = :ad_group_id
|
||||
GROUP BY ph.date_add
|
||||
ORDER BY ph.date_add DESC
|
||||
LIMIT ' . $start . ', ' . $limit,
|
||||
[
|
||||
':product_id' => (int) $product_id,
|
||||
':client_id' => (int) $client_id,
|
||||
':campaign_id' => (int) $campaign_id,
|
||||
':ad_group_id' => (int) $ad_group_id
|
||||
]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function get_records_total_product_history( $client_id, $product_id )
|
||||
static public function get_records_total_product_history( $client_id, $product_id, $campaign_id = 0, $ad_group_id = 0 )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> query( 'SELECT COUNT(0) FROM products_history AS ph WHERE ph.product_id = \'' . $product_id . '\'' ) -> fetchColumn();
|
||||
return $mdb -> query(
|
||||
'SELECT COUNT( DISTINCT ph.date_add )
|
||||
FROM products_history AS ph
|
||||
INNER JOIN products AS p ON p.id = ph.product_id
|
||||
WHERE ph.product_id = :product_id
|
||||
AND p.client_id = :client_id
|
||||
AND ph.campaign_id = :campaign_id
|
||||
AND ph.ad_group_id = :ad_group_id',
|
||||
[
|
||||
':product_id' => (int) $product_id,
|
||||
':client_id' => (int) $client_id,
|
||||
':campaign_id' => (int) $campaign_id,
|
||||
':ad_group_id' => (int) $ad_group_id
|
||||
]
|
||||
) -> fetchColumn();
|
||||
}
|
||||
|
||||
static public function get_product_history_30( $client_id, $product_id, $start, $limit )
|
||||
static public function get_product_history_30( $client_id, $product_id, $start, $limit, $campaign_id = 0, $ad_group_id = 0 )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> query( 'SELECT * FROM products_history_30 AS ph3 WHERE ph3.product_id = \'' . $product_id . '\' ORDER BY ph3.date_add ASC LIMIT ' . $start . ', ' . $limit ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
return $mdb -> query(
|
||||
'SELECT ph3.*
|
||||
FROM products_history_30 AS ph3
|
||||
INNER JOIN products AS p ON p.id = ph3.product_id
|
||||
WHERE ph3.product_id = :product_id
|
||||
AND p.client_id = :client_id
|
||||
AND ph3.campaign_id = :campaign_id
|
||||
AND ph3.ad_group_id = :ad_group_id
|
||||
ORDER BY ph3.date_add ASC
|
||||
LIMIT ' . (int) $start . ', ' . (int) $limit,
|
||||
[
|
||||
':product_id' => (int) $product_id,
|
||||
':client_id' => (int) $client_id,
|
||||
':campaign_id' => (int) $campaign_id,
|
||||
':ad_group_id' => (int) $ad_group_id
|
||||
]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
|
||||
static public function get_impressions_30( $product_id )
|
||||
static public function get_impressions_30( $product_id, $campaign_id = null, $ad_group_id = null )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> query( 'SELECT SUM(impressions) FROM products_history WHERE product_id = \'' . $product_id . '\' AND date_add >= \'' . date( 'Y-m-d', strtotime( '-30 days', time() ) ) . '\'' ) -> fetchColumn();
|
||||
|
||||
$sql = 'SELECT COALESCE( SUM( impressions ), 0 ) AS total
|
||||
FROM products_history
|
||||
WHERE product_id = :product_id
|
||||
AND date_add >= :date_from';
|
||||
|
||||
$params = [
|
||||
':product_id' => (int) $product_id,
|
||||
':date_from' => date( 'Y-m-d', strtotime( '-30 days', time() ) )
|
||||
];
|
||||
|
||||
if ( $campaign_id !== null )
|
||||
{
|
||||
$sql .= ' AND campaign_id = :campaign_id';
|
||||
$params[':campaign_id'] = (int) $campaign_id;
|
||||
}
|
||||
|
||||
if ( $ad_group_id !== null )
|
||||
{
|
||||
$sql .= ' AND ad_group_id = :ad_group_id';
|
||||
$params[':ad_group_id'] = (int) $ad_group_id;
|
||||
}
|
||||
|
||||
return $mdb -> query( $sql, $params ) -> fetchColumn();
|
||||
}
|
||||
|
||||
static public function get_clicks_30( $product_id )
|
||||
static public function get_clicks_30( $product_id, $campaign_id = null, $ad_group_id = null )
|
||||
{
|
||||
global $mdb;
|
||||
return $mdb -> query( 'SELECT SUM(clicks) FROM products_history WHERE product_id = \'' . $product_id . '\' AND date_add >= \'' . date( 'Y-m-d', strtotime( '-30 days', time() ) ) . '\'' ) -> fetchColumn();
|
||||
|
||||
$sql = 'SELECT COALESCE( SUM( clicks ), 0 ) AS total
|
||||
FROM products_history
|
||||
WHERE product_id = :product_id
|
||||
AND date_add >= :date_from';
|
||||
|
||||
$params = [
|
||||
':product_id' => (int) $product_id,
|
||||
':date_from' => date( 'Y-m-d', strtotime( '-30 days', time() ) )
|
||||
];
|
||||
|
||||
if ( $campaign_id !== null )
|
||||
{
|
||||
$sql .= ' AND campaign_id = :campaign_id';
|
||||
$params[':campaign_id'] = (int) $campaign_id;
|
||||
}
|
||||
|
||||
if ( $ad_group_id !== null )
|
||||
{
|
||||
$sql .= ' AND ad_group_id = :ad_group_id';
|
||||
$params[':ad_group_id'] = (int) $ad_group_id;
|
||||
}
|
||||
|
||||
return $mdb -> query( $sql, $params ) -> fetchColumn();
|
||||
}
|
||||
|
||||
static public function add_product_comment( $product_id, $comment, $date = null )
|
||||
@@ -183,4 +406,4 @@ class Products
|
||||
else
|
||||
return $mdb -> insert( 'products_comments', [ 'product_id' => $product_id, 'comment' => $comment, 'date_add' => $date ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,20 +449,49 @@ class GoogleAdsApi
|
||||
{
|
||||
$date = date( 'Y-m-d', strtotime( $date ) );
|
||||
|
||||
$gaql = "SELECT "
|
||||
. "segments.date, "
|
||||
. "segments.product_item_id, "
|
||||
. "segments.product_title, "
|
||||
. "metrics.impressions, "
|
||||
. "metrics.clicks, "
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM shopping_performance_view "
|
||||
. "WHERE segments.date = '" . $date . "'";
|
||||
$gaql_with_ad_group = "SELECT "
|
||||
. "segments.date, "
|
||||
. "segments.product_item_id, "
|
||||
. "segments.product_title, "
|
||||
. "campaign.id, "
|
||||
. "campaign.name, "
|
||||
. "ad_group.id, "
|
||||
. "ad_group.name, "
|
||||
. "metrics.impressions, "
|
||||
. "metrics.clicks, "
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM shopping_performance_view "
|
||||
. "WHERE segments.date = '" . $date . "'";
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql );
|
||||
if ( $results === false ) return false;
|
||||
$results = $this -> search_stream( $customer_id, $gaql_with_ad_group );
|
||||
$fallback_without_ad_group = false;
|
||||
|
||||
if ( $results === false )
|
||||
{
|
||||
$gaql_without_ad_group = "SELECT "
|
||||
. "segments.date, "
|
||||
. "segments.product_item_id, "
|
||||
. "segments.product_title, "
|
||||
. "campaign.id, "
|
||||
. "campaign.name, "
|
||||
. "metrics.impressions, "
|
||||
. "metrics.clicks, "
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM shopping_performance_view "
|
||||
. "WHERE segments.date = '" . $date . "'";
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql_without_ad_group );
|
||||
if ( $results === false )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$fallback_without_ad_group = true;
|
||||
}
|
||||
|
||||
$products = [];
|
||||
|
||||
@@ -474,11 +503,42 @@ class GoogleAdsApi
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( !isset( $products[ $offer_id ] ) )
|
||||
$campaign_id = (int) ( $row['campaign']['id'] ?? 0 );
|
||||
$campaign_name = trim( (string) ( $row['campaign']['name'] ?? '' ) );
|
||||
if ( $campaign_name === '' && $campaign_id > 0 )
|
||||
{
|
||||
$products[ $offer_id ] = [
|
||||
$campaign_name = 'Kampania #' . $campaign_id;
|
||||
}
|
||||
|
||||
$ad_group_id = 0;
|
||||
$ad_group_name = 'PMax (bez grup reklam)';
|
||||
|
||||
if ( !$fallback_without_ad_group )
|
||||
{
|
||||
$ad_group_id = (int) ( $row['adGroup']['id'] ?? 0 );
|
||||
$ad_group_name = trim( (string) ( $row['adGroup']['name'] ?? '' ) );
|
||||
|
||||
if ( $ad_group_id > 0 && $ad_group_name === '' )
|
||||
{
|
||||
$ad_group_name = 'Ad group #' . $ad_group_id;
|
||||
}
|
||||
else if ( $ad_group_id <= 0 )
|
||||
{
|
||||
$ad_group_name = 'PMax (bez grup reklam)';
|
||||
}
|
||||
}
|
||||
|
||||
$scope_key = $offer_id . '|' . $campaign_id . '|' . $ad_group_id;
|
||||
|
||||
if ( !isset( $products[ $scope_key ] ) )
|
||||
{
|
||||
$products[ $scope_key ] = [
|
||||
'OfferId' => $offer_id,
|
||||
'ProductTitle' => (string) ( $row['segments']['productTitle'] ?? $offer_id ),
|
||||
'CampaignId' => $campaign_id,
|
||||
'CampaignName' => $campaign_name,
|
||||
'AdGroupId' => $ad_group_id,
|
||||
'AdGroupName' => $ad_group_name,
|
||||
'Impressions' => 0,
|
||||
'Clicks' => 0,
|
||||
'Cost' => 0.0,
|
||||
@@ -487,11 +547,11 @@ class GoogleAdsApi
|
||||
];
|
||||
}
|
||||
|
||||
$products[ $offer_id ]['Impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
|
||||
$products[ $offer_id ]['Clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
|
||||
$products[ $offer_id ]['Cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
|
||||
$products[ $offer_id ]['Conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
|
||||
$products[ $offer_id ]['ConversionValue'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
|
||||
$products[ $scope_key ]['Impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
|
||||
$products[ $scope_key ]['Clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
|
||||
$products[ $scope_key ]['Cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
|
||||
$products[ $scope_key ]['Conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
|
||||
$products[ $scope_key ]['ConversionValue'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
|
||||
}
|
||||
|
||||
return array_values( $products );
|
||||
@@ -502,6 +562,7 @@ class GoogleAdsApi
|
||||
$gaql = "SELECT "
|
||||
. "campaign.id, "
|
||||
. "campaign.name, "
|
||||
. "campaign.advertising_channel_type, "
|
||||
. "campaign.bidding_strategy_type, "
|
||||
. "campaign.target_roas.target_roas, "
|
||||
. "campaign_budget.amount_micros, "
|
||||
@@ -526,6 +587,7 @@ class GoogleAdsApi
|
||||
$campaigns[ $cid ] = [
|
||||
'campaign_id' => $cid,
|
||||
'campaign_name' => $row['campaign']['name'] ?? '',
|
||||
'advertising_channel_type' => (string) ( $row['campaign']['advertisingChannelType'] ?? '' ),
|
||||
'bidding_strategy' => $row['campaign']['biddingStrategyType'] ?? 'UNKNOWN',
|
||||
'target_roas' => isset( $row['campaign']['targetRoas']['targetRoas'] )
|
||||
? (float) $row['campaign']['targetRoas']['targetRoas']
|
||||
@@ -601,8 +663,8 @@ class GoogleAdsApi
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM ad_group "
|
||||
. "WHERE campaign.status != 'REMOVED' "
|
||||
. "AND ad_group.status != 'REMOVED' "
|
||||
. "WHERE campaign.status = 'ENABLED' "
|
||||
. "AND ad_group.status = 'ENABLED' "
|
||||
. "AND segments.date DURING LAST_30_DAYS";
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql );
|
||||
@@ -623,8 +685,8 @@ class GoogleAdsApi
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM ad_group "
|
||||
. "WHERE campaign.status != 'REMOVED' "
|
||||
. "AND ad_group.status != 'REMOVED'";
|
||||
. "WHERE campaign.status = 'ENABLED' "
|
||||
. "AND ad_group.status = 'ENABLED'";
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql );
|
||||
if ( $results === false ) return false;
|
||||
@@ -653,7 +715,15 @@ class GoogleAdsApi
|
||||
$results = $this -> search_stream( $customer_id, $gaql );
|
||||
if ( $results === false ) return false;
|
||||
|
||||
return $this -> aggregate_search_terms( $results );
|
||||
$terms = $this -> aggregate_search_terms( $results );
|
||||
|
||||
$pmax_terms = $this -> get_pmax_search_terms_30_days( $customer_id );
|
||||
if ( $pmax_terms !== false && is_array( $pmax_terms ) && !empty( $pmax_terms ) )
|
||||
{
|
||||
$terms = array_merge( $terms, $pmax_terms );
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
public function get_search_terms_all_time( $customer_id )
|
||||
@@ -676,7 +746,58 @@ class GoogleAdsApi
|
||||
$results = $this -> search_stream( $customer_id, $gaql );
|
||||
if ( $results === false ) return false;
|
||||
|
||||
return $this -> aggregate_search_terms( $results );
|
||||
$terms = $this -> aggregate_search_terms( $results );
|
||||
|
||||
$pmax_terms = $this -> get_pmax_search_terms_all_time( $customer_id );
|
||||
if ( $pmax_terms !== false && is_array( $pmax_terms ) && !empty( $pmax_terms ) )
|
||||
{
|
||||
$terms = array_merge( $terms, $pmax_terms );
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
private function get_pmax_search_terms_30_days( $customer_id )
|
||||
{
|
||||
$gaql = "SELECT "
|
||||
. "campaign.id, "
|
||||
. "campaign_search_term_view.search_term, "
|
||||
. "metrics.impressions, "
|
||||
. "metrics.clicks, "
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM campaign_search_term_view "
|
||||
. "WHERE campaign.status != 'REMOVED' "
|
||||
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX' "
|
||||
. "AND metrics.clicks > 0 "
|
||||
. "AND segments.date DURING LAST_30_DAYS";
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql );
|
||||
if ( $results === false ) return false;
|
||||
|
||||
return $this -> aggregate_campaign_search_terms( $results );
|
||||
}
|
||||
|
||||
private function get_pmax_search_terms_all_time( $customer_id )
|
||||
{
|
||||
$gaql = "SELECT "
|
||||
. "campaign.id, "
|
||||
. "campaign_search_term_view.search_term, "
|
||||
. "metrics.impressions, "
|
||||
. "metrics.clicks, "
|
||||
. "metrics.cost_micros, "
|
||||
. "metrics.conversions, "
|
||||
. "metrics.conversions_value "
|
||||
. "FROM campaign_search_term_view "
|
||||
. "WHERE campaign.status != 'REMOVED' "
|
||||
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX' "
|
||||
. "AND metrics.clicks > 0";
|
||||
|
||||
$results = $this -> search_stream( $customer_id, $gaql );
|
||||
if ( $results === false ) return false;
|
||||
|
||||
return $this -> aggregate_campaign_search_terms( $results );
|
||||
}
|
||||
|
||||
public function get_negative_keywords( $customer_id )
|
||||
@@ -871,4 +992,59 @@ class GoogleAdsApi
|
||||
|
||||
return array_values( $terms );
|
||||
}
|
||||
|
||||
private function aggregate_campaign_search_terms( $results )
|
||||
{
|
||||
$terms = [];
|
||||
|
||||
foreach ( $results as $row )
|
||||
{
|
||||
$campaign_id = $row['campaign']['id'] ?? null;
|
||||
$search_term = trim( (string) ( $row['campaignSearchTermView']['searchTerm'] ?? '' ) );
|
||||
|
||||
if ( !$campaign_id || $search_term === '' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $campaign_id . '|0|' . strtolower( $search_term );
|
||||
|
||||
if ( !isset( $terms[ $key ] ) )
|
||||
{
|
||||
$terms[ $key ] = [
|
||||
'campaign_id' => (int) $campaign_id,
|
||||
'ad_group_id' => 0,
|
||||
'ad_group_name' => 'PMax (bez grup reklam)',
|
||||
'search_term' => $search_term,
|
||||
'impressions' => 0,
|
||||
'clicks' => 0,
|
||||
'cost' => 0.0,
|
||||
'conversions' => 0.0,
|
||||
'conversion_value' => 0.0,
|
||||
'roas' => 0.0
|
||||
];
|
||||
}
|
||||
|
||||
$terms[ $key ]['impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
|
||||
$terms[ $key ]['clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
|
||||
$terms[ $key ]['cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
|
||||
$terms[ $key ]['conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
|
||||
$terms[ $key ]['conversion_value'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
|
||||
}
|
||||
|
||||
foreach ( $terms as $key => &$term )
|
||||
{
|
||||
if ( (int) $term['clicks'] <= 0 )
|
||||
{
|
||||
unset( $terms[ $key ] );
|
||||
continue;
|
||||
}
|
||||
|
||||
$term['roas'] = ( $term['cost'] > 0 )
|
||||
? round( ( $term['conversion_value'] / $term['cost'] ) * 100, 2 )
|
||||
: 0;
|
||||
}
|
||||
|
||||
return array_values( $terms );
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -80,7 +80,7 @@ table {
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-family: "Roboto", sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 15px;
|
||||
@@ -158,7 +158,7 @@ input[type="checkbox"] {
|
||||
height: 35px;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-family: "Roboto", sans-serif;
|
||||
|
||||
option {
|
||||
padding: 5px;
|
||||
@@ -1406,4 +1406,4 @@ table {
|
||||
.jconfirm-box .select2-container--default .select2-results__option--highlighted[aria-selected] {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-family: "Roboto", sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
@@ -162,7 +162,7 @@ body.unlogged {
|
||||
border-radius: 8px;
|
||||
padding: 0 14px;
|
||||
font-size: 14px;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-family: "Roboto", sans-serif;
|
||||
color: #2D3748;
|
||||
transition: border-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
@@ -493,7 +493,7 @@ body.logged {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn.btn_small, .btn.btn-xs, .btn.btn-sm {
|
||||
@@ -532,7 +532,7 @@ body.logged {
|
||||
height: 38px;
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: 14px;
|
||||
color: #2D3748;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
@@ -1577,7 +1577,7 @@ table#products a.custom_name {
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-family: "Roboto", sans-serif;
|
||||
}
|
||||
.comment-form textarea {
|
||||
min-height: 120px;
|
||||
@@ -1676,3 +1676,4 @@ table#products a.custom_name {
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=style.css.map */
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ $transitionSpeed: 0.3s;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-family: "Roboto", sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
@@ -212,7 +212,7 @@ body.unlogged {
|
||||
border-radius: 8px;
|
||||
padding: 0 14px;
|
||||
font-size: 14px;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-family: "Roboto", sans-serif;
|
||||
color: $cTextDark;
|
||||
transition: border-color $transitionSpeed, box-shadow $transitionSpeed;
|
||||
|
||||
@@ -610,7 +610,7 @@ body.logged {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-weight: 500;
|
||||
|
||||
&.btn_small,
|
||||
@@ -661,7 +661,7 @@ body.logged {
|
||||
height: 38px;
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: 14px;
|
||||
color: $cTextDark;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
@@ -1882,7 +1882,7 @@ table#products {
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-family: "Roboto", sans-serif;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@@ -1989,4 +1989,4 @@ table#products {
|
||||
.main-wrapper {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
migrations/004_campaigns_performance_max_flag.sql
Normal file
18
migrations/004_campaigns_performance_max_flag.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- Migracja: typ kampanii Google Ads
|
||||
-- Data: 2026-02-17
|
||||
-- Opis: dodaje pole z dokladnym typem kampanii (SEARCH, DISPLAY, PERFORMANCE_MAX, itp.)
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'campaigns'
|
||||
AND COLUMN_NAME = 'advertising_channel_type'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `campaigns` ADD COLUMN `advertising_channel_type` VARCHAR(40) NULL DEFAULT NULL AFTER `campaign_name`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
18
migrations/005_drop_is_performance_max_column.sql
Normal file
18
migrations/005_drop_is_performance_max_column.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
-- Migracja: usuniecie przestarzalej flagi is_performance_max
|
||||
-- Data: 2026-02-17
|
||||
-- Opis: pozostawiamy tylko advertising_channel_type jako zrodlo prawdy o typie kampanii
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'campaigns'
|
||||
AND COLUMN_NAME = 'is_performance_max'
|
||||
),
|
||||
'ALTER TABLE `campaigns` DROP COLUMN `is_performance_max`',
|
||||
'DO 1'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
236
migrations/006_products_scope_dimensions.sql
Normal file
236
migrations/006_products_scope_dimensions.sql
Normal file
@@ -0,0 +1,236 @@
|
||||
-- Migracja: rozbicie statystyk produktow per kampania / grupa reklam
|
||||
-- Data: 2026-02-18
|
||||
-- Uwaga: products_data pozostaje globalne per product_id (bez podzialu na scope)
|
||||
|
||||
-- products_history.campaign_id
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_history'
|
||||
AND COLUMN_NAME = 'campaign_id'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_history` ADD COLUMN `campaign_id` INT(11) NOT NULL DEFAULT 0 AFTER `product_id`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- products_history.ad_group_id
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_history'
|
||||
AND COLUMN_NAME = 'ad_group_id'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_history` ADD COLUMN `ad_group_id` INT(11) NOT NULL DEFAULT 0 AFTER `campaign_id`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- products_temp.campaign_id
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_temp'
|
||||
AND COLUMN_NAME = 'campaign_id'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_temp` ADD COLUMN `campaign_id` INT(11) NOT NULL DEFAULT 0 AFTER `product_id`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- products_temp.ad_group_id
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_temp'
|
||||
AND COLUMN_NAME = 'ad_group_id'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_temp` ADD COLUMN `ad_group_id` INT(11) NOT NULL DEFAULT 0 AFTER `campaign_id`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- products_history_30.campaign_id
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_history_30'
|
||||
AND COLUMN_NAME = 'campaign_id'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_history_30` ADD COLUMN `campaign_id` INT(11) NOT NULL DEFAULT 0 AFTER `product_id`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- products_history_30.ad_group_id
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_history_30'
|
||||
AND COLUMN_NAME = 'ad_group_id'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_history_30` ADD COLUMN `ad_group_id` INT(11) NOT NULL DEFAULT 0 AFTER `campaign_id`'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- products_history: indeksy scope + dzien
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_history'
|
||||
AND INDEX_NAME = 'idx_products_history_campaign_id'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_history` ADD INDEX `idx_products_history_campaign_id` (`campaign_id`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_history'
|
||||
AND INDEX_NAME = 'idx_products_history_ad_group_id'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_history` ADD INDEX `idx_products_history_ad_group_id` (`ad_group_id`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_history'
|
||||
AND INDEX_NAME = 'uk_products_history_scope_day'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_history` ADD UNIQUE INDEX `uk_products_history_scope_day` (`product_id`, `campaign_id`, `ad_group_id`, `date_add`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_history_30'
|
||||
AND INDEX_NAME = 'idx_products_history_30_campaign_id'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_history_30` ADD INDEX `idx_products_history_30_campaign_id` (`campaign_id`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_history_30'
|
||||
AND INDEX_NAME = 'idx_products_history_30_ad_group_id'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_history_30` ADD INDEX `idx_products_history_30_ad_group_id` (`ad_group_id`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_history_30'
|
||||
AND INDEX_NAME = 'uk_products_history_30_scope_day'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_history_30` ADD UNIQUE INDEX `uk_products_history_30_scope_day` (`product_id`, `campaign_id`, `ad_group_id`, `date_add`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- products_temp: indeksy scope
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_temp'
|
||||
AND INDEX_NAME = 'idx_products_temp_campaign_id'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_temp` ADD INDEX `idx_products_temp_campaign_id` (`campaign_id`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_temp'
|
||||
AND INDEX_NAME = 'idx_products_temp_ad_group_id'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_temp` ADD INDEX `idx_products_temp_ad_group_id` (`ad_group_id`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'products_temp'
|
||||
AND INDEX_NAME = 'uk_products_temp_scope'
|
||||
),
|
||||
'DO 1',
|
||||
'ALTER TABLE `products_temp` ADD UNIQUE INDEX `uk_products_temp_scope` (`product_id`, `campaign_id`, `ad_group_id`)'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@@ -29,12 +29,32 @@
|
||||
<label for="phrase_search"><i class="fa-solid fa-magnifying-glass"></i> Szukaj frazy</label>
|
||||
<input type="text" id="phrase_search" class="form-control" placeholder="Wpisz fragment frazy..." />
|
||||
</div>
|
||||
<div class="filter-group terms-columns-group">
|
||||
<label><i class="fa-solid fa-table-columns"></i> Kolumny</label>
|
||||
<div class="terms-columns-box">
|
||||
<details class="terms-columns-control">
|
||||
<summary>Grupy reklam</summary>
|
||||
<div class="terms-columns-list" id="terms_columns_list_ad_groups"></div>
|
||||
</details>
|
||||
<details class="terms-columns-control">
|
||||
<summary>Frazy wyszukiwane</summary>
|
||||
<div class="terms-columns-list" id="terms_columns_list_search"></div>
|
||||
</details>
|
||||
<details class="terms-columns-control">
|
||||
<summary>Frazy wykluczajace</summary>
|
||||
<div class="terms-columns-list" id="terms_columns_list_negative"></div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="campaign-terms-wrap">
|
||||
<div class="campaigns-extra-card terms-card terms-adgroups-card">
|
||||
<div class="campaigns-extra-card-title">
|
||||
<i class="fa-solid fa-layer-group"></i> Grupy reklam
|
||||
<span class="terms-card-title-label"><i class="fa-solid fa-layer-group"></i> Grupy reklam</span>
|
||||
<button type="button" id="terms_adgroups_toggle" class="terms-card-toggle" title="Zwin tabele grup reklam" aria-label="Zwin tabele grup reklam" aria-expanded="true">
|
||||
<i class="fa-solid fa-chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="campaigns-extra-table-wrap">
|
||||
<table class="table table-sm campaigns-extra-table" id="terms_ad_groups_table">
|
||||
@@ -62,20 +82,25 @@
|
||||
<div class="campaigns-extra-card-title">
|
||||
<i class="fa-solid fa-magnifying-glass"></i> Frazy wyszukiwane (klikniecia >= 1)
|
||||
</div>
|
||||
<div class="terms-search-toolbar">
|
||||
<label class="terms-search-toolbar-label"><i class="fa-solid fa-filter"></i> Klik. all</label>
|
||||
<input type="number" id="terms_min_clicks_all" class="form-control" min="0" step="1" placeholder=">= np. 100" />
|
||||
<input type="number" id="terms_max_clicks_all" class="form-control" min="0" step="1" placeholder="<= np. 500" />
|
||||
</div>
|
||||
<div class="campaigns-extra-table-wrap">
|
||||
<table class="table table-sm campaigns-extra-table" id="terms_search_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Fraza</th>
|
||||
<th>Grupa reklam</th>
|
||||
<th>Klik. 30d</th>
|
||||
<th>Koszt 30d</th>
|
||||
<th>Wartosc 30d</th>
|
||||
<th>ROAS 30d</th>
|
||||
<th>Klik. all</th>
|
||||
<th>Koszt all</th>
|
||||
<th>Wartosc all</th>
|
||||
<th>ROAS all</th>
|
||||
<th>Klik. 30d</th>
|
||||
<th>Koszt 30d</th>
|
||||
<th>Wartosc 30d</th>
|
||||
<th>ROAS 30d</th>
|
||||
<th>Akcja</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -121,6 +146,61 @@
|
||||
.campaign-terms-page .campaigns-filters .filter-group {
|
||||
min-width: 220px;
|
||||
}
|
||||
.campaign-terms-page .campaigns-filters .filter-group.terms-columns-group {
|
||||
min-width: 280px;
|
||||
}
|
||||
.terms-columns-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.terms-columns-control {
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 6px;
|
||||
background: #FFFFFF;
|
||||
overflow: hidden;
|
||||
}
|
||||
.terms-columns-control summary {
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
list-style: none;
|
||||
}
|
||||
.terms-columns-control summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.terms-columns-control summary::after {
|
||||
content: '\25BC';
|
||||
float: right;
|
||||
font-size: 10px;
|
||||
color: #64748B;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.terms-columns-control[open] summary::after {
|
||||
content: '\25B2';
|
||||
}
|
||||
.terms-columns-list {
|
||||
border-top: 1px solid #EEF2F7;
|
||||
padding: 8px 10px;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.terms-columns-list .terms-col-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.terms-columns-list .terms-col-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.terms-columns-list .terms-col-item input[type=checkbox] {
|
||||
margin: 0;
|
||||
}
|
||||
.campaign-terms-page {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
@@ -141,6 +221,58 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.campaigns-extra-card-title .terms-card-title-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.campaign-terms-page .terms-card-toggle {
|
||||
margin-left: auto;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid #E2E8F0;
|
||||
border-radius: 6px;
|
||||
background: #FFFFFF;
|
||||
color: #475569;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.campaign-terms-page .terms-card-toggle:hover {
|
||||
background: #F8FAFC;
|
||||
border-color: #CBD5E1;
|
||||
}
|
||||
.campaign-terms-page .terms-adgroups-card.is-collapsed .campaigns-extra-table-wrap {
|
||||
display: none;
|
||||
}
|
||||
.campaign-terms-page .terms-search-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #EEF2F7;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
.campaign-terms-page .terms-search-toolbar label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.campaign-terms-page .terms-search-toolbar .terms-search-toolbar-label {
|
||||
min-width: 86px;
|
||||
}
|
||||
.campaign-terms-page .terms-search-toolbar #terms_min_clicks_all,
|
||||
.campaign-terms-page .terms-search-toolbar #terms_max_clicks_all {
|
||||
width: 160px;
|
||||
height: 32px;
|
||||
}
|
||||
.campaigns-extra-table-wrap {
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -149,20 +281,74 @@
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.campaigns-extra-table thead th {
|
||||
.campaign-terms-page table.campaigns-extra-table > thead > tr > th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
background: #F8FAFC;
|
||||
border-bottom: 1px solid #E2E8F0;
|
||||
background-color: #111827 !important;
|
||||
color: #E5E7EB !important;
|
||||
border-bottom: 1px solid #0B1220 !important;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .4px;
|
||||
color: #64748B;
|
||||
padding: 10px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.campaign-terms-page #terms_search_table thead th .dt-column-order,
|
||||
.campaign-terms-page #terms_negative_table thead th .dt-column-order {
|
||||
display: none !important;
|
||||
}
|
||||
.campaign-terms-page #terms_search_table thead th.dt-orderable-asc,
|
||||
.campaign-terms-page #terms_search_table thead th.dt-orderable-desc,
|
||||
.campaign-terms-page #terms_negative_table thead th.dt-orderable-asc,
|
||||
.campaign-terms-page #terms_negative_table thead th.dt-orderable-desc {
|
||||
cursor: pointer;
|
||||
padding-right: 34px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.campaign-terms-page #terms_search_table thead th .dt-column-title,
|
||||
.campaign-terms-page #terms_negative_table thead th .dt-column-title {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 2px;
|
||||
}
|
||||
.campaign-terms-page #terms_search_table thead th.dt-orderable-asc::after,
|
||||
.campaign-terms-page #terms_search_table thead th.dt-orderable-desc::after,
|
||||
.campaign-terms-page #terms_negative_table thead th.dt-orderable-asc::after,
|
||||
.campaign-terms-page #terms_negative_table thead th.dt-orderable-desc::after {
|
||||
content: '\2195';
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
color: #E5E7EB;
|
||||
background: #374151;
|
||||
}
|
||||
.campaign-terms-page #terms_search_table thead th.dt-ordering-asc::after,
|
||||
.campaign-terms-page #terms_negative_table thead th.dt-ordering-asc::after,
|
||||
.campaign-terms-page #terms_search_table thead th[aria-sort="ascending"]::after,
|
||||
.campaign-terms-page #terms_negative_table thead th[aria-sort="ascending"]::after {
|
||||
content: '\25B2';
|
||||
color: #FFFFFF;
|
||||
background: #2563EB;
|
||||
}
|
||||
.campaign-terms-page #terms_search_table thead th.dt-ordering-desc::after,
|
||||
.campaign-terms-page #terms_negative_table thead th.dt-ordering-desc::after,
|
||||
.campaign-terms-page #terms_search_table thead th[aria-sort="descending"]::after,
|
||||
.campaign-terms-page #terms_negative_table thead th[aria-sort="descending"]::after {
|
||||
content: '\25BC';
|
||||
color: #FFFFFF;
|
||||
background: #2563EB;
|
||||
}
|
||||
.campaigns-extra-table tbody td {
|
||||
padding: 9px 12px;
|
||||
border-bottom: 1px solid #F1F5F9;
|
||||
@@ -288,6 +474,58 @@ var terms_negative_table = null;
|
||||
var TERMS_STORAGE_CLIENT = 'campaign_terms.last_client_id';
|
||||
var TERMS_STORAGE_CAMPAIGN = 'campaign_terms.last_campaign_id';
|
||||
var TERMS_STORAGE_AD_GROUP = 'campaign_terms.last_ad_group_id';
|
||||
var TERMS_STORAGE_SEARCH_TABLE_PAGE_PREFIX = 'campaign_terms.search_table_page.';
|
||||
var TERMS_STORAGE_NEGATIVE_TABLE_PAGE_PREFIX = 'campaign_terms.negative_table_page.';
|
||||
var TERMS_STORAGE_COLUMNS_PREFIX = 'campaign_terms.columns.';
|
||||
var TERMS_STORAGE_AD_GROUPS_COLLAPSED = 'campaign_terms.ad_groups_collapsed';
|
||||
var TERMS_STORAGE_MIN_CLICKS_ALL = 'campaign_terms.min_clicks_all';
|
||||
var TERMS_STORAGE_MAX_CLICKS_ALL = 'campaign_terms.max_clicks_all';
|
||||
|
||||
var terms_search_filter_registered = false;
|
||||
|
||||
var TERMS_COLUMNS_LABELS = {
|
||||
ad_groups: [
|
||||
'Grupa reklam',
|
||||
'Klik. 30d',
|
||||
'Koszt 30d',
|
||||
'Wartosc 30d',
|
||||
'ROAS 30d',
|
||||
'Klik. all',
|
||||
'Koszt all',
|
||||
'Wartosc all',
|
||||
'ROAS all'
|
||||
],
|
||||
search: [
|
||||
'Fraza',
|
||||
'Grupa reklam',
|
||||
'Klik. all',
|
||||
'Koszt all',
|
||||
'Wartosc all',
|
||||
'ROAS all',
|
||||
'Klik. 30d',
|
||||
'Koszt 30d',
|
||||
'Wartosc 30d',
|
||||
'ROAS 30d',
|
||||
'Akcja'
|
||||
],
|
||||
negative: [
|
||||
'Poziom',
|
||||
'Fraza',
|
||||
'Match type'
|
||||
]
|
||||
};
|
||||
|
||||
var TERMS_COLUMNS_LIST_SELECTORS = {
|
||||
ad_groups: '#terms_columns_list_ad_groups',
|
||||
search: '#terms_columns_list_search',
|
||||
negative: '#terms_columns_list_negative'
|
||||
};
|
||||
|
||||
var TERMS_COLUMNS_TABLE_SELECTORS = {
|
||||
ad_groups: '#terms_ad_groups_table',
|
||||
search: '#terms_search_table',
|
||||
negative: '#terms_negative_table'
|
||||
};
|
||||
|
||||
function terms_storage_set( key, value )
|
||||
{
|
||||
@@ -313,6 +551,279 @@ function terms_storage_get( key )
|
||||
}
|
||||
}
|
||||
|
||||
function terms_pagination_context()
|
||||
{
|
||||
var client_id = $( '#terms_client_id' ).val() || '0';
|
||||
var campaign_id = $( '#terms_campaign_id' ).val() || '0';
|
||||
var ad_group_id = $( '#terms_ad_group_id' ).val() || '0';
|
||||
return [ client_id, campaign_id, ad_group_id ].join( ':' );
|
||||
}
|
||||
|
||||
function terms_page_storage_key( table_type )
|
||||
{
|
||||
var prefix = table_type === 'negative' ? TERMS_STORAGE_NEGATIVE_TABLE_PAGE_PREFIX : TERMS_STORAGE_SEARCH_TABLE_PAGE_PREFIX;
|
||||
return prefix + terms_pagination_context();
|
||||
}
|
||||
|
||||
function terms_columns_storage_key( table_key )
|
||||
{
|
||||
if ( table_key === 'search' )
|
||||
return TERMS_STORAGE_COLUMNS_PREFIX + table_key + '.v2';
|
||||
|
||||
return TERMS_STORAGE_COLUMNS_PREFIX + table_key;
|
||||
}
|
||||
|
||||
function terms_get_table_instance( table_key )
|
||||
{
|
||||
if ( table_key === 'ad_groups' ) return terms_ad_groups_table;
|
||||
if ( table_key === 'search' ) return terms_search_table;
|
||||
if ( table_key === 'negative' ) return terms_negative_table;
|
||||
return null;
|
||||
}
|
||||
|
||||
function terms_get_saved_columns_visibility( table_key, columns_count )
|
||||
{
|
||||
var raw = terms_storage_get( terms_columns_storage_key( table_key ) );
|
||||
if ( !raw )
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var saved = JSON.parse( raw );
|
||||
if ( !Array.isArray( saved ) || saved.length !== columns_count )
|
||||
return null;
|
||||
|
||||
return saved.map( function( visible ) { return !!visible; } );
|
||||
}
|
||||
catch ( e )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function terms_save_columns_visibility( table_key, table_instance )
|
||||
{
|
||||
if ( !table_instance || !table_instance.columns )
|
||||
return;
|
||||
|
||||
var columns_count = table_instance.columns().count();
|
||||
var visible_map = [];
|
||||
var i = 0;
|
||||
|
||||
for ( i = 0; i < columns_count; i++ )
|
||||
{
|
||||
visible_map.push( table_instance.column( i ).visible() );
|
||||
}
|
||||
|
||||
terms_storage_set( terms_columns_storage_key( table_key ), JSON.stringify( visible_map ) );
|
||||
}
|
||||
|
||||
function terms_apply_saved_columns_visibility( table_key, table_instance )
|
||||
{
|
||||
if ( !table_instance || !table_instance.columns )
|
||||
return;
|
||||
|
||||
var columns_count = table_instance.columns().count();
|
||||
var saved_visibility = terms_get_saved_columns_visibility( table_key, columns_count );
|
||||
var i = 0;
|
||||
|
||||
if ( !saved_visibility )
|
||||
return;
|
||||
|
||||
for ( i = 0; i < columns_count; i++ )
|
||||
{
|
||||
table_instance.column( i ).visible( saved_visibility[i], false );
|
||||
}
|
||||
|
||||
table_instance.columns.adjust().draw( false );
|
||||
}
|
||||
|
||||
function terms_render_columns_picker( table_key, table_instance )
|
||||
{
|
||||
var selector = TERMS_COLUMNS_LIST_SELECTORS[table_key] || '';
|
||||
var labels = TERMS_COLUMNS_LABELS[table_key] || [];
|
||||
var columns_count = ( table_instance && table_instance.columns ) ? table_instance.columns().count() : 0;
|
||||
var $list = $( selector );
|
||||
var i = 0;
|
||||
|
||||
if ( !$list.length )
|
||||
return;
|
||||
|
||||
$list.empty();
|
||||
|
||||
if ( !columns_count )
|
||||
{
|
||||
$list.append( '<div class="terms-col-item">Brak kolumn.</div>' );
|
||||
return;
|
||||
}
|
||||
|
||||
for ( i = 0; i < columns_count; i++ )
|
||||
{
|
||||
var label = labels[i] || ( 'Kolumna ' + ( i + 1 ) );
|
||||
var checked = table_instance.column( i ).visible() ? ' checked' : '';
|
||||
var id = 'terms_col_' + table_key + '_' + i;
|
||||
|
||||
$list.append(
|
||||
'<label class="terms-col-item" for="' + id + '">' +
|
||||
'<input type="checkbox" class="terms-col-toggle" id="' + id + '" data-table-key="' + table_key + '" data-col-index="' + i + '"' + checked + '>' +
|
||||
'<span>' + label + '</span>' +
|
||||
'</label>'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function terms_get_clicks_filter_value( selector )
|
||||
{
|
||||
var raw = $( selector ).val();
|
||||
|
||||
if ( raw === '' || raw === null || raw === undefined )
|
||||
return null;
|
||||
|
||||
var n = parseInt( raw, 10 );
|
||||
|
||||
if ( isNaN( n ) )
|
||||
return null;
|
||||
|
||||
if ( n < 0 )
|
||||
n = 0;
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
function terms_get_min_clicks_all()
|
||||
{
|
||||
return terms_get_clicks_filter_value( '#terms_min_clicks_all' );
|
||||
}
|
||||
|
||||
function terms_get_max_clicks_all()
|
||||
{
|
||||
return terms_get_clicks_filter_value( '#terms_max_clicks_all' );
|
||||
}
|
||||
|
||||
function terms_register_search_filters()
|
||||
{
|
||||
var ext_search = null;
|
||||
|
||||
if ( terms_search_filter_registered )
|
||||
return;
|
||||
|
||||
if ( $.fn.dataTable && $.fn.dataTable.ext && $.fn.dataTable.ext.search )
|
||||
ext_search = $.fn.dataTable.ext.search;
|
||||
else if ( typeof DataTable !== 'undefined' && DataTable.ext && DataTable.ext.search )
|
||||
ext_search = DataTable.ext.search;
|
||||
|
||||
if ( !ext_search )
|
||||
return;
|
||||
|
||||
ext_search.push( function( settings, data )
|
||||
{
|
||||
var table_id = settings && settings.nTable ? settings.nTable.id : '';
|
||||
var min_clicks = terms_get_min_clicks_all();
|
||||
var max_clicks = terms_get_max_clicks_all();
|
||||
|
||||
if ( table_id !== 'terms_search_table' )
|
||||
return true;
|
||||
|
||||
if ( min_clicks === null && max_clicks === null )
|
||||
return true;
|
||||
|
||||
var clicks_raw = data && data.length > 2 ? String( data[2] ) : '0';
|
||||
var clicks = parseInt( clicks_raw.replace( /[^\d-]/g, '' ), 10 );
|
||||
|
||||
if ( isNaN( clicks ) )
|
||||
clicks = 0;
|
||||
|
||||
if ( min_clicks !== null && clicks < min_clicks )
|
||||
return false;
|
||||
|
||||
if ( max_clicks !== null && clicks > max_clicks )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
} );
|
||||
|
||||
terms_search_filter_registered = true;
|
||||
}
|
||||
|
||||
function terms_set_ad_groups_collapsed( collapsed )
|
||||
{
|
||||
var $card = $( '.terms-adgroups-card' );
|
||||
var $toggle = $( '#terms_adgroups_toggle' );
|
||||
|
||||
if ( !$card.length || !$toggle.length )
|
||||
return;
|
||||
|
||||
if ( collapsed )
|
||||
{
|
||||
$card.addClass( 'is-collapsed' );
|
||||
$toggle.attr( 'title', 'Rozwin tabele grup reklam' );
|
||||
$toggle.attr( 'aria-label', 'Rozwin tabele grup reklam' );
|
||||
$toggle.attr( 'aria-expanded', 'false' );
|
||||
$toggle.find( 'i' ).attr( 'class', 'fa-solid fa-chevron-down' );
|
||||
}
|
||||
else
|
||||
{
|
||||
$card.removeClass( 'is-collapsed' );
|
||||
$toggle.attr( 'title', 'Zwin tabele grup reklam' );
|
||||
$toggle.attr( 'aria-label', 'Zwin tabele grup reklam' );
|
||||
$toggle.attr( 'aria-expanded', 'true' );
|
||||
$toggle.find( 'i' ).attr( 'class', 'fa-solid fa-chevron-up' );
|
||||
}
|
||||
|
||||
terms_storage_set( TERMS_STORAGE_AD_GROUPS_COLLAPSED, collapsed ? '1' : '0' );
|
||||
}
|
||||
|
||||
function terms_restore_ad_groups_collapsed()
|
||||
{
|
||||
var saved = terms_storage_get( TERMS_STORAGE_AD_GROUPS_COLLAPSED );
|
||||
terms_set_ad_groups_collapsed( saved === '1' );
|
||||
}
|
||||
|
||||
function terms_get_saved_page( table_type )
|
||||
{
|
||||
var page_raw = terms_storage_get( terms_page_storage_key( table_type ) );
|
||||
var page = parseInt( page_raw, 10 );
|
||||
|
||||
if ( isNaN( page ) || page < 0 )
|
||||
return 0;
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
function terms_save_current_page( table_type, table_instance )
|
||||
{
|
||||
if ( !table_instance || typeof table_instance.page !== 'function' )
|
||||
return;
|
||||
|
||||
var info = table_instance.page.info();
|
||||
if ( !info )
|
||||
return;
|
||||
|
||||
terms_storage_set( terms_page_storage_key( table_type ), info.page );
|
||||
}
|
||||
|
||||
function terms_restore_saved_page( table_type, table_instance )
|
||||
{
|
||||
if ( !table_instance || typeof table_instance.page !== 'function' )
|
||||
return;
|
||||
|
||||
var info = table_instance.page.info();
|
||||
if ( !info || !info.pages )
|
||||
return;
|
||||
|
||||
var saved_page = terms_get_saved_page( table_type );
|
||||
var page_to_set = saved_page;
|
||||
|
||||
if ( page_to_set >= info.pages )
|
||||
page_to_set = info.pages - 1;
|
||||
|
||||
if ( page_to_set < 0 )
|
||||
page_to_set = 0;
|
||||
|
||||
if ( page_to_set !== info.page )
|
||||
table_instance.page( page_to_set ).draw( 'page' );
|
||||
}
|
||||
|
||||
function format_num( value, digits )
|
||||
{
|
||||
var n = Number( value || 0 );
|
||||
@@ -331,6 +842,21 @@ function destroy_table_if_exists( selector )
|
||||
}
|
||||
}
|
||||
|
||||
function apply_header_titles( selector )
|
||||
{
|
||||
$( selector + ' thead th' ).each( function()
|
||||
{
|
||||
var $th = $( this );
|
||||
var title = $.trim( $th.find( '.dt-column-title' ).first().text() || $th.text() );
|
||||
|
||||
if ( title )
|
||||
{
|
||||
$th.attr( 'title', title );
|
||||
$th.find( '.dt-column-title' ).attr( 'title', title );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
|
||||
function build_ad_groups_table( rows )
|
||||
{
|
||||
@@ -370,6 +896,8 @@ function build_ad_groups_table( rows )
|
||||
}
|
||||
});
|
||||
|
||||
terms_apply_saved_columns_visibility( 'ad_groups', terms_ad_groups_table );
|
||||
terms_render_columns_picker( 'ad_groups', terms_ad_groups_table );
|
||||
}
|
||||
|
||||
function build_search_terms_table( rows, negative_keywords )
|
||||
@@ -390,18 +918,18 @@ function build_search_terms_table( rows, negative_keywords )
|
||||
lengthChange: false,
|
||||
pageLength: 15,
|
||||
pagingType: 'simple_numbers',
|
||||
order: [],
|
||||
order: [[ 2, 'desc' ]],
|
||||
columns: [
|
||||
{ data: 'search_term', defaultContent: '', width: '340px' },
|
||||
{ data: 'ad_group_name', defaultContent: '', width: '360px' },
|
||||
{ data: 'clicks_30', render: function( data, type ){ return type === 'display' ? format_num( data, 0 ) : Number( data || 0 ); } },
|
||||
{ data: 'cost_30', render: function( data, type ){ return type === 'display' ? format_num( data, 2 ) : Number( data || 0 ); } },
|
||||
{ data: 'conversion_value_30', render: function( data, type ){ return type === 'display' ? format_num( data, 2 ) : Number( data || 0 ); } },
|
||||
{ data: 'roas_30', render: function( data, type ){ return type === 'display' ? format_num( data, 2 ) + '%' : Number( data || 0 ); } },
|
||||
{ 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 ); } },
|
||||
{ data: 'roas_all_time', render: function( data, type ){ return type === 'display' ? format_num( data, 2 ) + '%' : Number( data || 0 ); } },
|
||||
{ data: 'clicks_30', render: function( data, type ){ return type === 'display' ? format_num( data, 0 ) : Number( data || 0 ); } },
|
||||
{ data: 'cost_30', render: function( data, type ){ return type === 'display' ? format_num( data, 2 ) : Number( data || 0 ); } },
|
||||
{ data: 'conversion_value_30', render: function( data, type ){ return type === 'display' ? format_num( data, 2 ) : Number( data || 0 ); } },
|
||||
{ data: 'roas_30', render: function( data, type ){ return type === 'display' ? format_num( data, 2 ) + '%' : Number( data || 0 ); } },
|
||||
{ data: 'id', orderable: false, searchable: false, render: function( data, type, row ) {
|
||||
if ( type !== 'display' ) return data;
|
||||
return '<button type="button" class="terms-add-negative-btn" data-search-term-id="' + row.id + '" title="Dodaj do wykluczajacych"><i class="fa-solid fa-ban"></i></button>';
|
||||
@@ -425,13 +953,25 @@ function build_search_terms_table( rows, negative_keywords )
|
||||
language: {
|
||||
emptyTable: 'Brak danych do wyswietlenia',
|
||||
info: 'Wpisy _START_ - _END_ z _TOTAL_',
|
||||
search: 'Szukaj:',
|
||||
paginate: { next: 'Dalej', previous: 'Wstecz' }
|
||||
}
|
||||
});
|
||||
|
||||
terms_apply_saved_columns_visibility( 'search', terms_search_table );
|
||||
|
||||
$( '#terms_search_table' )
|
||||
.off( 'page.dt.termsPagination' )
|
||||
.on( 'page.dt.termsPagination', function()
|
||||
{
|
||||
terms_save_current_page( 'search', terms_search_table );
|
||||
} );
|
||||
|
||||
var q = $( '#phrase_search' ).val();
|
||||
terms_search_table.search( q ).draw();
|
||||
terms_restore_saved_page( 'search', terms_search_table );
|
||||
apply_header_titles( '#terms_search_table' );
|
||||
terms_render_columns_picker( 'search', terms_search_table );
|
||||
}
|
||||
|
||||
function build_negative_terms_table( rows )
|
||||
@@ -466,13 +1006,25 @@ function build_negative_terms_table( rows )
|
||||
language: {
|
||||
emptyTable: 'Brak danych do wyswietlenia',
|
||||
info: 'Wpisy _START_ - _END_ z _TOTAL_',
|
||||
search: 'Szukaj:',
|
||||
paginate: { next: 'Dalej', previous: 'Wstecz' }
|
||||
}
|
||||
});
|
||||
|
||||
terms_apply_saved_columns_visibility( 'negative', terms_negative_table );
|
||||
|
||||
$( '#terms_negative_table' )
|
||||
.off( 'page.dt.termsPagination' )
|
||||
.on( 'page.dt.termsPagination', function()
|
||||
{
|
||||
terms_save_current_page( 'negative', terms_negative_table );
|
||||
} );
|
||||
|
||||
var q = $( '#phrase_search' ).val();
|
||||
terms_negative_table.search( q ).draw();
|
||||
terms_restore_saved_page( 'negative', terms_negative_table );
|
||||
apply_header_titles( '#terms_negative_table' );
|
||||
terms_render_columns_picker( 'negative', terms_negative_table );
|
||||
}
|
||||
|
||||
function reset_all_tables()
|
||||
@@ -600,6 +1152,15 @@ function load_campaigns_for_client( restore_campaign_id )
|
||||
|
||||
$( function()
|
||||
{
|
||||
terms_register_search_filters();
|
||||
|
||||
var saved_min_clicks_all = terms_storage_get( TERMS_STORAGE_MIN_CLICKS_ALL );
|
||||
if ( saved_min_clicks_all !== '' )
|
||||
$( '#terms_min_clicks_all' ).val( saved_min_clicks_all );
|
||||
var saved_max_clicks_all = terms_storage_get( TERMS_STORAGE_MAX_CLICKS_ALL );
|
||||
if ( saved_max_clicks_all !== '' )
|
||||
$( '#terms_max_clicks_all' ).val( saved_max_clicks_all );
|
||||
|
||||
$( 'body' ).on( 'change', '#terms_client_id', function()
|
||||
{
|
||||
var client_id = $( this ).val();
|
||||
@@ -627,6 +1188,8 @@ $( function()
|
||||
$( 'body' ).on( 'click', '.terms-add-negative-btn', function()
|
||||
{
|
||||
var search_term_id = $( this ).data( 'search-term-id' );
|
||||
var row_data = terms_search_table ? terms_search_table.row( $( this ).closest( 'tr' ) ).data() : null;
|
||||
var selected_search_term = row_data ? String( row_data.search_term || '' ) : '';
|
||||
|
||||
$.confirm({
|
||||
title: 'Dodaj do wykluczajacych',
|
||||
@@ -647,8 +1210,17 @@ $( function()
|
||||
'<option value="EXACT">Dopasowanie scisle</option>' +
|
||||
'<option value="BROAD">Dopasowanie przyblizone</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="form-group" style="margin-top:10px;margin-bottom:0;">' +
|
||||
'<label for="negative_keyword_text" style="display:block;margin-bottom:6px;">Fraza wykluczajaca</label>' +
|
||||
'<input id="negative_keyword_text" type="text" class="form-control" placeholder="Wpisz fraze do wykluczenia..." />' +
|
||||
'<small style="display:block;margin-top:6px;color:#64748B;">Mozesz zmienic fraze przed zapisem, np. usunac jej fragment.</small>' +
|
||||
'</div>',
|
||||
type: 'blue',
|
||||
onContentReady: function()
|
||||
{
|
||||
this.$content.find( '#negative_keyword_text' ).val( selected_search_term );
|
||||
},
|
||||
buttons: {
|
||||
confirm: {
|
||||
text: 'Zapisz',
|
||||
@@ -657,7 +1229,20 @@ $( function()
|
||||
{
|
||||
var match_type = this.$content.find( '#negative_match_type' ).val() || 'PHRASE';
|
||||
var scope = this.$content.find( '#negative_scope' ).val() || 'campaign';
|
||||
var keyword_text = $.trim( this.$content.find( '#negative_keyword_text' ).val() || '' );
|
||||
var modal = this;
|
||||
|
||||
if ( keyword_text === '' )
|
||||
{
|
||||
$.alert({
|
||||
title: 'Uwaga',
|
||||
columnClass: 'col-md-4 col-md-offset-4',
|
||||
content: 'Wpisz fraze, ktora ma zostac dodana do wykluczajacych.',
|
||||
type: 'orange'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
modal.showLoading( true );
|
||||
|
||||
$.ajax({
|
||||
@@ -666,7 +1251,8 @@ $( function()
|
||||
data: {
|
||||
search_term_id: search_term_id,
|
||||
match_type: match_type,
|
||||
scope: scope
|
||||
scope: scope,
|
||||
keyword_text: keyword_text
|
||||
},
|
||||
success: function( response )
|
||||
{
|
||||
@@ -679,16 +1265,18 @@ $( function()
|
||||
if ( data.debug )
|
||||
{
|
||||
debugHtml = '<hr style="margin:10px 0;">' +
|
||||
'<div style="font-size:11px;color:#666;text-align:left;">' +
|
||||
'<strong>Debug:</strong><br>' +
|
||||
'Customer ID: ' + ( data.debug.customer_id || 'brak' ) + '<br>' +
|
||||
'Campaign ID: ' + ( data.debug.campaign_external_id || 'brak' ) + '<br>' +
|
||||
'Ad Group ID: ' + ( data.debug.ad_group_external_id || 'brak' ) + '<br>' +
|
||||
'Scope: ' + ( data.debug.scope || 'brak' ) + '<br>' +
|
||||
'Keyword: ' + ( data.debug.keyword_text || 'brak' ) + '<br>' +
|
||||
'API response: <pre style="font-size:10px;max-height:120px;overflow:auto;background:#f5f5f5;padding:6px;margin-top:4px;">' + JSON.stringify( data.debug.api_response, null, 2 ) + '</pre>' +
|
||||
'Verification: <pre style="font-size:10px;max-height:120px;overflow:auto;background:#f5f5f5;padding:6px;margin-top:4px;">' + JSON.stringify( data.debug.verification, null, 2 ) + '</pre>' +
|
||||
'</div>';
|
||||
'<details style="font-size:11px;color:#666;text-align:left;">' +
|
||||
'<summary style="cursor:pointer;font-weight:700;">Debug</summary>' +
|
||||
'<div style="margin-top:8px;">' +
|
||||
'Customer ID: ' + ( data.debug.customer_id || 'brak' ) + '<br>' +
|
||||
'Campaign ID: ' + ( data.debug.campaign_external_id || 'brak' ) + '<br>' +
|
||||
'Ad Group ID: ' + ( data.debug.ad_group_external_id || 'brak' ) + '<br>' +
|
||||
'Scope: ' + ( data.debug.scope || 'brak' ) + '<br>' +
|
||||
'Keyword: ' + ( data.debug.keyword_text || 'brak' ) + '<br>' +
|
||||
'API response: <pre style="font-size:10px;max-height:120px;overflow:auto;background:#f5f5f5;padding:6px;margin-top:4px;">' + JSON.stringify( data.debug.api_response, null, 2 ) + '</pre>' +
|
||||
'Verification: <pre style="font-size:10px;max-height:120px;overflow:auto;background:#f5f5f5;padding:6px;margin-top:4px;">' + JSON.stringify( data.debug.verification, null, 2 ) + '</pre>' +
|
||||
'</div>' +
|
||||
'</details>';
|
||||
}
|
||||
var successDialog = $.alert({
|
||||
title: 'Sukces',
|
||||
@@ -707,9 +1295,12 @@ $( function()
|
||||
if ( data.debug )
|
||||
{
|
||||
debugHtml = '<hr style="margin:10px 0;">' +
|
||||
'<div style="font-size:11px;color:#666;text-align:left;">' +
|
||||
'<strong>Debug:</strong><pre style="font-size:10px;max-height:150px;overflow:auto;background:#f5f5f5;padding:6px;">' + JSON.stringify( data.debug, null, 2 ) + '</pre>' +
|
||||
'</div>';
|
||||
'<details style="font-size:11px;color:#666;text-align:left;">' +
|
||||
'<summary style="cursor:pointer;font-weight:700;">Debug</summary>' +
|
||||
'<div style="margin-top:8px;">' +
|
||||
'<pre style="font-size:10px;max-height:150px;overflow:auto;background:#f5f5f5;padding:6px;">' + JSON.stringify( data.debug, null, 2 ) + '</pre>' +
|
||||
'</div>' +
|
||||
'</details>';
|
||||
}
|
||||
$.alert({
|
||||
title: 'Blad',
|
||||
@@ -748,6 +1339,44 @@ $( function()
|
||||
terms_negative_table.search( q ).draw();
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'input change', '#terms_min_clicks_all, #terms_max_clicks_all', function()
|
||||
{
|
||||
terms_storage_set( TERMS_STORAGE_MIN_CLICKS_ALL, $( '#terms_min_clicks_all' ).val() );
|
||||
terms_storage_set( TERMS_STORAGE_MAX_CLICKS_ALL, $( '#terms_max_clicks_all' ).val() );
|
||||
if ( terms_search_table )
|
||||
terms_search_table.draw();
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'change', '.terms-col-toggle', function()
|
||||
{
|
||||
var table_key = $( this ).data( 'table-key' );
|
||||
var col_index = parseInt( $( this ).data( 'col-index' ), 10 );
|
||||
var is_visible = $( this ).is( ':checked' );
|
||||
var table = terms_get_table_instance( table_key );
|
||||
var selector = TERMS_COLUMNS_TABLE_SELECTORS[table_key] || '';
|
||||
|
||||
if ( !table || isNaN( col_index ) )
|
||||
return;
|
||||
|
||||
table.column( col_index ).visible( is_visible, false );
|
||||
table.columns.adjust().draw( false );
|
||||
terms_save_columns_visibility( table_key, table );
|
||||
|
||||
if ( selector )
|
||||
apply_header_titles( selector );
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'click', '#terms_adgroups_toggle', function()
|
||||
{
|
||||
var was_collapsed = $( '.terms-adgroups-card' ).hasClass( 'is-collapsed' );
|
||||
|
||||
terms_set_ad_groups_collapsed( !was_collapsed );
|
||||
|
||||
if ( was_collapsed && terms_ad_groups_table )
|
||||
terms_ad_groups_table.columns.adjust().draw( false );
|
||||
});
|
||||
|
||||
terms_restore_ad_groups_collapsed();
|
||||
reset_all_tables();
|
||||
|
||||
var saved_client_id = terms_storage_get( TERMS_STORAGE_CLIENT );
|
||||
|
||||
@@ -110,7 +110,7 @@ function reloadChart()
|
||||
|
||||
Highcharts.chart( 'container', {
|
||||
chart: {
|
||||
style: { fontFamily: '"Open Sans", sans-serif' },
|
||||
style: { fontFamily: '"Roboto", sans-serif' },
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
title: { text: '' },
|
||||
@@ -205,6 +205,15 @@ $( function()
|
||||
{
|
||||
campaigns_select.val( campaign_to_restore ).trigger( 'change' );
|
||||
}
|
||||
else
|
||||
{
|
||||
var account_option = campaigns_select.find( 'option' ).filter( function() {
|
||||
return $.trim( $( this ).text() ).toLowerCase() === '--- konto ---';
|
||||
} ).first();
|
||||
|
||||
if ( account_option.length )
|
||||
campaigns_select.val( account_option.val() ).trigger( 'change' );
|
||||
}
|
||||
|
||||
restore_campaign_after_client_load = '';
|
||||
}
|
||||
@@ -388,3 +397,4 @@ $( function()
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,6 +14,18 @@
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group filter-group-campaign">
|
||||
<label for="products_campaign_id"><i class="fa-solid fa-bullhorn"></i> Kampania</label>
|
||||
<select id="products_campaign_id" name="products_campaign_id" class="form-control">
|
||||
<option value="">- wszystkie kampanie -</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group filter-group-ad-group">
|
||||
<label for="products_ad_group_id"><i class="fa-solid fa-layer-group"></i> Grupa reklam</label>
|
||||
<select id="products_ad_group_id" name="products_ad_group_id" class="form-control">
|
||||
<option value="">- wszystkie grupy -</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group filter-group-roas">
|
||||
<label for="bestseller_min_roas"><i class="fa-solid fa-star"></i> Bestseller min ROAS</label>
|
||||
<input type="text" id="bestseller_min_roas" name="bestseller_min_roas" class="form-control" placeholder="np. 500" value="" />
|
||||
@@ -35,6 +47,8 @@
|
||||
<th><input type="checkbox" id="select-all-products" title="Zaznacz wszystkie" /></th>
|
||||
<th>Id</th>
|
||||
<th>Id oferty</th>
|
||||
<th>Kampania</th>
|
||||
<th>Grupa reklam</th>
|
||||
<th>Nazwa produktu</th>
|
||||
<th>Wyśw.</th>
|
||||
<th>Wyśw. (30d)</th>
|
||||
@@ -111,82 +125,188 @@ function loadGoogleCategories( callback )
|
||||
|
||||
$( function()
|
||||
{
|
||||
// Załaduj produkty po wyborze klienta
|
||||
$( 'body' ).on( 'change', '#client_id', function()
|
||||
var products_table = new DataTable( '#products', {
|
||||
ajax: {
|
||||
type: 'POST',
|
||||
url: '/products/get_products/',
|
||||
data: function( d ) {
|
||||
d.client_id = $( '#client_id' ).val() || '';
|
||||
d.campaign_id = $( '#products_campaign_id' ).val() || '';
|
||||
d.ad_group_id = $( '#products_ad_group_id' ).val() || '';
|
||||
}
|
||||
},
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
autoWidth: false,
|
||||
searching: false,
|
||||
lengthChange: false,
|
||||
pageLength: 25,
|
||||
columns: [
|
||||
{ width: '30px', orderable: false, className: 'select-checkbox', render: function( data, type, row ) {
|
||||
return '<input type="checkbox" class="product-checkbox" value="' + row[1] + '" />';
|
||||
}
|
||||
},
|
||||
{ width: '50px', orderable: false },
|
||||
{ width: '80px', name: 'offer_id' },
|
||||
{ width: '200px', name: 'campaign_name' },
|
||||
{ width: '200px', name: 'ad_group_name' },
|
||||
{ name: 'name' },
|
||||
{ width: '50px', name: 'impressions' },
|
||||
{ width: '80px', name: 'impressions_30' },
|
||||
{ width: '50px', name: 'clicks' },
|
||||
{ width: '80px', name: 'clicks_30' },
|
||||
{ width: '50px', name: 'ctr' },
|
||||
{ width: '80px', name: 'cost', className: "dt-type-numeric" },
|
||||
{ width: '50px', name: 'cpc', className: "dt-type-numeric" },
|
||||
{ width: '50px', name: 'conversions' },
|
||||
{ width: '90px', name: 'conversions_value', className: "dt-type-numeric" },
|
||||
{ width: '60px', name: 'roas' },
|
||||
{ width: '70px', name: 'min_roas' },
|
||||
{ width: '50px', name: 'cl3', orderable: false },
|
||||
{ width: '120px', orderable: false },
|
||||
{ width: '50px', orderable: false, className: 'dt-center' }
|
||||
],
|
||||
order: [ [ 8, 'desc' ] ],
|
||||
language: {
|
||||
processing: '£adowanie...',
|
||||
emptyTable: 'Brak produktów do wyœwietlenia',
|
||||
info: 'Produkty _START_ - _END_ z _TOTAL_',
|
||||
infoEmpty: '',
|
||||
paginate: {
|
||||
first: 'Pierwsza',
|
||||
last: 'Ostatnia',
|
||||
next: 'Dalej',
|
||||
previous: 'Wstecz'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function reload_products_table()
|
||||
{
|
||||
var client_id = $( this ).val();
|
||||
localStorage.setItem( 'products_client_id', client_id );
|
||||
products_table.ajax.reload( null, false );
|
||||
}
|
||||
|
||||
table = $( '#products' ).DataTable();
|
||||
table.destroy();
|
||||
function load_client_bestseller_min_roas( client_id )
|
||||
{
|
||||
if ( !client_id )
|
||||
{
|
||||
$( '#bestseller_min_roas' ).val( '' );
|
||||
return;
|
||||
}
|
||||
|
||||
// Pobierz min ROAS bestsellera
|
||||
$.ajax({
|
||||
url: '/products/get_client_bestseller_min_roas/',
|
||||
type: 'POST',
|
||||
data: { client_id: client_id },
|
||||
success: function( response ) {
|
||||
data = JSON.parse( response );
|
||||
var data = JSON.parse( response );
|
||||
$( '#bestseller_min_roas' ).val( data.status == 'ok' ? data.min_roas : '' );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
new DataTable( '#products', {
|
||||
ajax: {
|
||||
type: 'POST',
|
||||
url: '/products/get_products/client_id=' + client_id,
|
||||
},
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
autoWidth: false,
|
||||
searching: false,
|
||||
lengthChange: false,
|
||||
pageLength: 25,
|
||||
columns: [
|
||||
{ width: '30px', orderable: false, className: 'select-checkbox', render: function( data, type, row ) {
|
||||
return '<input type="checkbox" class="product-checkbox" value="' + row[1] + '" />';
|
||||
}
|
||||
},
|
||||
{ width: '50px', orderable: false },
|
||||
{ width: '80px', name: 'offer_id' },
|
||||
{ name: 'name' },
|
||||
{ width: '50px', name: 'impressions' },
|
||||
{ width: '80px', name: 'impressions_30' },
|
||||
{ width: '50px', name: 'clicks' },
|
||||
{ width: '80px', name: 'clicks_30' },
|
||||
{ width: '50px', name: 'ctr' },
|
||||
{ width: '80px', name: 'cost', className: "dt-type-numeric" },
|
||||
{ width: '50px', name: 'cpc', className: "dt-type-numeric" },
|
||||
{ width: '50px', name: 'conversions' },
|
||||
{ width: '90px', name: 'conversions_value', className: "dt-type-numeric" },
|
||||
{ width: '60px', name: 'roas' },
|
||||
{ width: '70px', name: 'min_roas' },
|
||||
{ width: '50px', name: 'cl3', orderable: false },
|
||||
{ width: '120px', orderable: false },
|
||||
{ width: '50px', orderable: false, className: 'dt-center' }
|
||||
],
|
||||
order: [ [ 6, 'desc' ] ],
|
||||
language: {
|
||||
processing: 'Ładowanie...',
|
||||
emptyTable: 'Brak produktów do wyświetlenia',
|
||||
info: 'Produkty _START_ - _END_ z _TOTAL_',
|
||||
infoEmpty: '',
|
||||
paginate: {
|
||||
first: 'Pierwsza',
|
||||
last: 'Ostatnia',
|
||||
next: 'Dalej',
|
||||
previous: 'Wstecz'
|
||||
}
|
||||
function load_products_campaigns( client_id, selected_campaign_id )
|
||||
{
|
||||
var $campaign = $( '#products_campaign_id' );
|
||||
$campaign.empty().append( '<option value="">- wszystkie kampanie -</option>' );
|
||||
|
||||
if ( !client_id )
|
||||
{
|
||||
return $.Deferred().resolve().promise();
|
||||
}
|
||||
|
||||
return $.ajax({
|
||||
url: '/products/get_campaigns_list/client_id=' + client_id,
|
||||
type: 'GET',
|
||||
dataType: 'json'
|
||||
}).done( function( res ) {
|
||||
( res.campaigns || [] ).forEach( function( row ) {
|
||||
$campaign.append( '<option value="' + row.id + '">' + row.campaign_name + '</option>' );
|
||||
} );
|
||||
|
||||
if ( selected_campaign_id && $campaign.find( 'option[value="' + selected_campaign_id + '"]' ).length )
|
||||
{
|
||||
$campaign.val( selected_campaign_id );
|
||||
}
|
||||
});
|
||||
} );
|
||||
}
|
||||
|
||||
function load_products_ad_groups( campaign_id, selected_ad_group_id )
|
||||
{
|
||||
var $ad_group = $( '#products_ad_group_id' );
|
||||
$ad_group.empty().append( '<option value="">- wszystkie grupy -</option>' );
|
||||
|
||||
if ( !campaign_id )
|
||||
{
|
||||
return $.Deferred().resolve().promise();
|
||||
}
|
||||
|
||||
return $.ajax({
|
||||
url: '/products/get_campaign_ad_groups/campaign_id=' + campaign_id,
|
||||
type: 'GET',
|
||||
dataType: 'json'
|
||||
}).done( function( res ) {
|
||||
( res.ad_groups || [] ).forEach( function( row ) {
|
||||
$ad_group.append( '<option value="' + row.id + '">' + row.ad_group_name + '</option>' );
|
||||
} );
|
||||
|
||||
if ( selected_ad_group_id && $ad_group.find( 'option[value="' + selected_ad_group_id + '"]' ).length )
|
||||
{
|
||||
$ad_group.val( selected_ad_group_id );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
$( 'body' ).on( 'change', '#client_id', function()
|
||||
{
|
||||
var client_id = $( this ).val() || '';
|
||||
localStorage.setItem( 'products_client_id', client_id );
|
||||
localStorage.removeItem( 'products_campaign_id' );
|
||||
localStorage.removeItem( 'products_ad_group_id' );
|
||||
|
||||
load_client_bestseller_min_roas( client_id );
|
||||
load_products_campaigns( client_id, '' ).done( function() {
|
||||
load_products_ad_groups( '', '' ).done( function() {
|
||||
reload_products_table();
|
||||
} );
|
||||
} );
|
||||
});
|
||||
|
||||
// Przywróć ostatnio wybranego klienta
|
||||
var savedClient = localStorage.getItem( 'products_client_id' );
|
||||
if ( savedClient && $( '#client_id option[value="' + savedClient + '"]' ).length ) {
|
||||
$( '#client_id' ).val( savedClient ).trigger( 'change' );
|
||||
$( 'body' ).on( 'change', '#products_campaign_id', function()
|
||||
{
|
||||
var campaign_id = $( this ).val() || '';
|
||||
localStorage.setItem( 'products_campaign_id', campaign_id );
|
||||
localStorage.removeItem( 'products_ad_group_id' );
|
||||
|
||||
load_products_ad_groups( campaign_id, '' ).done( function() {
|
||||
reload_products_table();
|
||||
} );
|
||||
});
|
||||
|
||||
$( 'body' ).on( 'change', '#products_ad_group_id', function()
|
||||
{
|
||||
var ad_group_id = $( this ).val() || '';
|
||||
localStorage.setItem( 'products_ad_group_id', ad_group_id );
|
||||
reload_products_table();
|
||||
});
|
||||
|
||||
var savedClient = localStorage.getItem( 'products_client_id' ) || '';
|
||||
var savedCampaign = localStorage.getItem( 'products_campaign_id' ) || '';
|
||||
var savedAdGroup = localStorage.getItem( 'products_ad_group_id' ) || '';
|
||||
|
||||
if ( savedClient && $( '#client_id option[value="' + savedClient + '"]' ).length )
|
||||
{
|
||||
$( '#client_id' ).val( savedClient );
|
||||
}
|
||||
|
||||
load_client_bestseller_min_roas( $( '#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() {
|
||||
reload_products_table();
|
||||
} );
|
||||
});
|
||||
|
||||
// Usuwanie produktu
|
||||
$( 'body' ).on( 'click', '.delete-product', function( e )
|
||||
{
|
||||
@@ -575,3 +695,7 @@ $( function()
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
$(function() {
|
||||
var client_id = <?= $this -> client_id;?>;
|
||||
var product_id = <?= $this -> product_id;?>;
|
||||
var campaign_id = <?= (int) ( $this -> campaign_id ?? 0 ); ?>;
|
||||
var ad_group_id = <?= (int) ( $this -> ad_group_id ?? 0 ); ?>;
|
||||
|
||||
// Ustaw domyślnie dzisiejszą datę w formularzu (YYYY-MM-DD)
|
||||
(function presetToday() {
|
||||
@@ -75,7 +77,7 @@
|
||||
new DataTable('#products', {
|
||||
ajax: {
|
||||
type: 'POST',
|
||||
url: '/products/get_product_history_table/client_id=' + client_id + '&product_id=' + product_id,
|
||||
url: '/products/get_product_history_table/client_id=' + client_id + '&product_id=' + product_id + '&campaign_id=' + campaign_id + '&ad_group_id=' + ad_group_id,
|
||||
},
|
||||
pageLength: 30,
|
||||
processing: true,
|
||||
@@ -100,7 +102,7 @@
|
||||
$.ajax({
|
||||
url: '/products/get_product_history_table_chart/',
|
||||
method: 'POST',
|
||||
data: { client_id: client_id, product_id: product_id },
|
||||
data: { client_id: client_id, product_id: product_id, campaign_id: campaign_id, ad_group_id: ad_group_id },
|
||||
success: function(response) {
|
||||
const parsedData = JSON.parse(response);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<meta name="robots" content="all">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.min.js"></script>
|
||||
<script type="text/javascript" src="/libraries/framework/vendor/plugins/datepicker/js/bootstrap-datetimepicker.js"></script>
|
||||
@@ -130,3 +130,4 @@
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<link href="/layout/favicon.png" rel="icon" type="image/x-icon">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js"></script>
|
||||
@@ -186,3 +186,4 @@
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/layout/style.css">
|
||||
@@ -42,3 +42,4 @@
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user