feat: Dodaj moduł grup i fraz, oznaczanie wykluczonych na czerwono, CLAUDE.md

- Nowy moduł CampaignTerms z widokiem grup reklam, fraz wyszukiwanych i fraz wykluczających
- Frazy wyszukiwane dodane do wykluczonych oznaczane czerwonym kolorem w tabeli
- Instalator migracji (install.php) z obsługą schema_migrations
- Migracja 003 dla tabel campaign_ad_groups, campaign_search_terms, campaign_negative_keywords
- CLAUDE.md z dokumentacją architektury projektu
- Aktualizacja layoutu, stylów i konfiguracji

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 23:44:30 +01:00
parent 59c086384b
commit 5e491d626c
23 changed files with 4739 additions and 251 deletions

View File

@@ -9,7 +9,9 @@
"WebFetch(domain:www.storegrowers.com)", "WebFetch(domain:www.storegrowers.com)",
"WebFetch(domain:platform.openai.com)", "WebFetch(domain:platform.openai.com)",
"WebFetch(domain:openai.com)", "WebFetch(domain:openai.com)",
"Bash(sass:*)" "Bash(sass:*)",
"WebFetch(domain:developers.google.com)",
"Bash(cd:*)"
] ]
} }
} }

99
CLAUDE.md Normal file
View File

@@ -0,0 +1,99 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
adsPRO is a PHP SaaS application for managing Google Ads campaigns, products, and clients. It integrates with Google Ads API, OpenAI, and Claude AI to provide AI-powered ad optimization. UI language is Polish.
## Architecture
Custom lightweight MVC framework with three layers:
- **Controllers** (`autoload/controls/class.*.php`, namespace `\controls`) - handle requests, return rendered views or JSON
- **Factories** (`autoload/factory/class.*.php`, namespace `\factory`) - data access layer, all static methods, use Medoo ORM
- **Views** (`autoload/view/class.*.php`, namespace `\view`) - compose templates with data
- **Services** (`autoload/services/class.*.php`, namespace `\services`) - external API integrations (GoogleAdsApi, ClaudeApi, OpenAiApi)
- **Templates** (`templates/`) - PHP files, variables accessed via `$this->varName`
### Autoloading
PSR-0-like: `\controls\Campaigns` resolves to `autoload/controls/class.Campaigns.php`.
### Routing
Entry point: `index.php`. URL `/module/action/key=value` maps to `\controls\Module::action()`. Route aliases defined in `$route_aliases` array. Default route: `campaigns/main_view`.
### Database
MySQL via Medoo ORM (`global $mdb`). Common patterns:
```php
$mdb->select('table', '*', ['field' => $value]);
$mdb->get('table', '*', ['id' => $id]);
$mdb->insert('table', ['field' => $value]);
$mdb->update('table', ['field' => $value], ['id' => $id]);
$mdb->query($sql, [':param' => $value])->fetchAll(\PDO::FETCH_ASSOC);
```
### Key Utility Classes
- `\S` - static helpers: `\S::get('param')` (POST/GET), `\S::get_session()`, `\S::set_session()`, `\S::alert()`, `\S::send_email()`
- `\Tpl::view('path/template', ['var' => $data])` - template rendering
- `\Html::input()`, `\Html::select()`, etc. - form component builders
- `\Cache::store()`, `\Cache::fetch()` - file-based caching
### Authentication
Session-based with cookie auto-login. User stored in `$_SESSION['user']`. Public paths whitelisted in `index.php`. IP validated per session.
## Commands
### Database Migrations
```bash
# Run migrations (via browser or CLI)
php install.php
# With demo data
php install.php --with_demo
# Force re-run all
php install.php --force
```
Migration files in `migrations/` follow pattern `NNN_description.sql`. Tracked in `schema_migrations` table (idempotent).
### SASS Compilation
VS Code Live Sass Compiler watches `layout/style.scss` and compiles to `layout/style.css` (compressed).
### Deployment
Files auto-upload to remote server via VS Code FTP-Kr extension (`.vscode/ftp-kr.json`). No build step required.
## Code Conventions
- **PHP style**: Spaces inside parentheses `if ( $x )`, braces on new line, 2-space indent in templates, 4-space in classes
- **Naming**: Classes PascalCase, methods/variables/columns snake_case, namespaces lowercase
- **Static methods**: Controllers and factories use `static public function`
- **JSON endpoints**: `echo json_encode([...]); exit;`
- **Template variables**: passed as array to `\Tpl::view()`, accessed as `$this->varName`
## Frontend Stack
jQuery 3.6, DataTables 2.1, Bootstrap 4, Select2 4.1, Highcharts, Font Awesome 6.5, jquery-confirm for modals. All loaded via CDN or from `libraries/`.
## Entry Points
| File | Purpose |
|------|---------|
| `index.php` | Main app (routing + auth) |
| `ajax.php` | AJAX requests (authenticated) |
| `api.php` | Public API |
| `cron.php` | Background jobs |
| `install.php` | Database migration runner |
| `config.php` | DB and email credentials |
## API Settings Storage
Google Ads, Claude, and OpenAI API keys are stored in the `settings` table (key-value) and managed via the Settings page (`\controls\Users::settings`).

View File

@@ -1,4 +1,4 @@
<? <?php
namespace controls; namespace controls;
class Api class Api
{ {
@@ -270,4 +270,257 @@ class Api
echo json_encode( [ 'status' => 'ok' ] ); echo json_encode( [ 'status' => 'ok' ] );
exit; exit;
} }
static public function products_data_import()
{
global $mdb;
$json = file_get_contents( 'php://input' );
$data = json_decode( $json, true );
if ( !is_array( $data ) || empty( $data['client_id'] ) || empty( $data['date'] ) || !isset( $data['data'] ) || !is_array( $data['data'] ) )
{
echo json_encode( [ 'status' => 'error', 'message' => 'Nieprawidlowe dane wejsciowe. Oczekiwano: client_id, date, data[].' ] );
exit;
}
$client_id = (int) $data['client_id'];
$date = date( 'Y-m-d', strtotime( $data['date'] ) );
if ( !$mdb -> count( 'clients', [ 'id' => $client_id ] ) )
{
echo json_encode( [ 'status' => 'error', 'message' => 'Nie znaleziono klienta o podanym ID.' ] );
exit;
}
$processed = 0;
$skipped = 0;
$touched_product_ids = [];
foreach ( $data['data'] as $offer )
{
$offer_external_id = trim( (string) ( $offer['OfferId'] ?? '' ) );
if ( $offer_external_id === '' )
{
$skipped++;
continue;
}
$product_title = trim( (string) ( $offer['ProductTitle'] ?? '' ) );
if ( $product_title === '' )
{
$product_title = $offer_external_id;
}
if ( !$mdb -> count( 'products', [ 'AND' => [ 'client_id' => $client_id, 'offer_id' => $offer_external_id ] ] ) )
{
$mdb -> insert( 'products', [
'client_id' => $client_id,
'offer_id' => $offer_external_id,
'name' => $product_title
] );
$product_id = $mdb -> id();
}
else
{
$product_id = $mdb -> get( 'products', 'id', [ 'AND' => [ 'client_id' => $client_id, 'offer_id' => $offer_external_id ] ] );
$offer_current_name = $mdb -> get( 'products', 'name', [ 'AND' => [ 'client_id' => $client_id, 'offer_id' => $offer_external_id ] ] );
if ( $offer_current_name != $product_title and $date == date( 'Y-m-d', strtotime( '-1 days', time() ) ) )
{
$mdb -> update( 'products', [ 'name' => $product_title ], [ 'AND' => [ 'client_id' => $client_id, 'offer_id' => $offer_external_id ] ] );
}
}
if ( !$product_id )
{
$skipped++;
continue;
}
$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 );
$conversions = self::normalize_number( $offer['Conversions'] ?? 0 );
$conversion_value = self::normalize_number( $offer['ConversionValue'] ?? 0 );
$ctr = ( $impressions > 0 ) ? round( $clicks / $impressions, 4 ) * 100 : 0;
$offer_data = [
'impressions' => $impressions,
'clicks' => $clicks,
'ctr' => $ctr,
'cost' => $cost,
'conversions' => $conversions,
'conversions_value' => $conversion_value,
'updated' => 1
];
if ( $mdb -> count( 'products_history', [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] ) )
{
$offer_data_old = $mdb -> get( 'products_history', '*', [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] );
if (
$offer_data_old['impressions'] == $offer_data['impressions']
and $offer_data_old['clicks'] == $offer_data['clicks']
and number_format( (float) str_replace( ',', '.', $offer_data_old['cost'] ), 5 ) == number_format( (float) $offer_data['cost'], 5 )
and (float) $offer_data_old['conversions'] == (float) $offer_data['conversions']
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;
$processed++;
continue;
}
$mdb -> update( 'products_history', $offer_data, [
'AND' => [ 'product_id' => $product_id, 'date_add' => $date ]
] );
}
else
{
$offer_data['product_id'] = $product_id;
$offer_data['date_add'] = $date;
$mdb -> insert( 'products_history', $offer_data );
}
$touched_product_ids[ $product_id ] = true;
$processed++;
}
$history_30_rows = 0;
foreach ( array_keys( $touched_product_ids ) as $product_id )
{
\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 ] ] );
$history_30_rows++;
}
$temp_rows = self::rebuild_products_temp_for_client( $client_id );
echo json_encode( [
'status' => 'ok',
'client_id' => $client_id,
'date' => $date,
'processed' => $processed,
'skipped' => $skipped,
'history_30_products' => $history_30_rows,
'products_temp_rows' => $temp_rows
] );
exit;
}
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;
}
static private function normalize_number( $value )
{
if ( $value === null || $value === '' )
{
return 0.0;
}
if ( is_int( $value ) || is_float( $value ) )
{
return (float) $value;
}
$value = trim( (string) $value );
if ( $value === '' )
{
return 0.0;
}
$value = preg_replace( '/[^\d,.\-]/', '', $value );
if ( $value === '' || $value === '-' || $value === ',' || $value === '.' )
{
return 0.0;
}
$has_comma = strpos( $value, ',' ) !== false;
$has_dot = strpos( $value, '.' ) !== false;
if ( $has_comma && $has_dot )
{
$last_comma = strrpos( $value, ',' );
$last_dot = strrpos( $value, '.' );
if ( $last_comma > $last_dot )
{
$value = str_replace( '.', '', $value );
$value = str_replace( ',', '.', $value );
}
else
{
$value = str_replace( ',', '', $value );
}
}
else
{
$value = str_replace( ',', '.', $value );
}
return (float) $value;
}
} }

View File

@@ -0,0 +1,177 @@
<?php
namespace controls;
class CampaignTerms
{
static public function main_view()
{
return \Tpl::view( 'campaign_terms/main_view', [
'clients' => \factory\Campaigns::get_clients(),
] );
}
static public function get_campaigns_list()
{
$client_id = (int) \S::get( 'client_id' );
echo json_encode( [ 'campaigns' => \factory\Campaigns::get_campaigns_list( $client_id ) ] );
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 get_campaign_phrase_details()
{
$campaign_id = (int) \S::get( 'campaign_id' );
$ad_group_id = (int) \S::get( 'ad_group_id' );
if ( $campaign_id <= 0 )
{
echo json_encode( [ 'search_terms' => [], 'negative_keywords' => [] ] );
exit;
}
echo json_encode( [
'search_terms' => \factory\Campaigns::get_campaign_search_terms( $campaign_id, $ad_group_id ),
'negative_keywords' => \factory\Campaigns::get_campaign_negative_keywords( $campaign_id, $ad_group_id )
] );
exit;
}
static public function add_negative_keyword()
{
$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' ) ) );
if ( $search_term_id <= 0 )
{
echo json_encode( [ 'success' => false, 'message' => 'Nie podano frazy do wykluczenia.' ] );
exit;
}
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
{
$match_type = 'PHRASE';
}
if ( !in_array( $scope, [ 'campaign', 'ad_group' ], true ) )
{
$scope = 'campaign';
}
$context = \factory\Campaigns::get_search_term_context( $search_term_id );
if ( !$context )
{
echo json_encode( [ 'success' => false, 'message' => 'Nie znaleziono danych frazy.' ] );
exit;
}
$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'] ?? '' ) );
$missing_data = ( $customer_id === '' || $keyword_text === '' );
if ( $scope === 'campaign' && $campaign_external_id === '' )
{
$missing_data = true;
}
if ( $scope === 'ad_group' && $ad_group_external_id === '' )
{
$missing_data = true;
}
if ( $missing_data )
{
echo json_encode( [
'success' => false,
'message' => 'Brak wymaganych danych Google Ads dla tej frazy.',
'debug' => [
'customer_id' => $customer_id,
'campaign_external_id' => $campaign_external_id,
'ad_group_external_id' => $ad_group_external_id,
'keyword_text' => $keyword_text,
'scope' => $scope,
'context' => $context
]
] );
exit;
}
$api = new \services\GoogleAdsApi();
if ( !$api -> is_configured() )
{
echo json_encode( [ 'success' => false, 'message' => 'Google Ads API nie jest skonfigurowane.' ] );
exit;
}
if ( $scope === 'campaign' )
{
$api_result = $api -> add_negative_keyword_to_campaign( $customer_id, $campaign_external_id, $keyword_text, $match_type );
}
else
{
$api_result = $api -> add_negative_keyword_to_ad_group( $customer_id, $ad_group_external_id, $keyword_text, $match_type );
}
if ( !( $api_result['success'] ?? false ) )
{
$last_error = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
echo json_encode( [
'success' => false,
'message' => 'Nie udalo sie zapisac frazy wykluczajacej w Google Ads.',
'error' => $last_error,
'debug' => [
'customer_id' => $customer_id,
'campaign_external_id' => $campaign_external_id,
'ad_group_external_id' => $ad_group_external_id,
'keyword_text' => $keyword_text,
'match_type' => $match_type,
'scope' => $scope,
'api_result' => $api_result
]
] );
exit;
}
\factory\Campaigns::upsert_campaign_negative_keyword(
(int) $context['db_campaign_id'],
$scope === 'campaign' ? null : (int) $context['db_ad_group_id'],
$scope,
$keyword_text,
$match_type
);
$scope_label = $scope === 'campaign' ? 'kampanii' : 'grupy reklam';
echo json_encode( [
'success' => true,
'message' => ( $api_result['duplicate'] ?? false ) ? 'Fraza byla juz wykluczona na poziomie ' . $scope_label . '.' : 'Fraza zostala dodana do wykluczajacych na poziomie ' . $scope_label . '.',
'duplicate' => (bool) ( $api_result['duplicate'] ?? false ),
'match_type' => $match_type,
'scope' => $scope,
'debug' => [
'customer_id' => $customer_id,
'campaign_external_id' => $campaign_external_id,
'ad_group_external_id' => $ad_group_external_id,
'keyword_text' => $keyword_text,
'scope' => $scope,
'api_response' => $api_result['response'] ?? null,
'sent_operation' => $api_result['sent_operation'] ?? null,
'verification' => $api_result['verification'] ?? null
]
] );
exit;
}
}

View File

@@ -104,6 +104,38 @@ class Campaigns
exit; 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 get_campaign_phrase_details()
{
$campaign_id = (int) \S::get( 'campaign_id' );
$ad_group_id = (int) \S::get( 'ad_group_id' );
if ( $campaign_id <= 0 )
{
echo json_encode( [ 'search_terms' => [], 'negative_keywords' => [] ] );
exit;
}
echo json_encode( [
'search_terms' => \factory\Campaigns::get_campaign_search_terms( $campaign_id, $ad_group_id ),
'negative_keywords' => \factory\Campaigns::get_campaign_negative_keywords( $campaign_id, $ad_group_id )
] );
exit;
}
static public function delete_campaign() static public function delete_campaign()
{ {
$campaign_id = \S::get( 'campaign_id' ); $campaign_id = \S::get( 'campaign_id' );

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,177 @@ class Campaigns
return $mdb -> get( 'clients', 'name', [ 'id' => $client_id ] ); return $mdb -> get( 'clients', 'name', [ 'id' => $client_id ] );
} }
static public function get_campaign_ad_groups( $campaign_id )
{
global $mdb;
return $mdb -> query(
'SELECT
id,
campaign_id,
ad_group_id,
ad_group_name,
impressions_30,
clicks_30,
cost_30,
conversions_30,
conversion_value_30,
roas_30,
impressions_all_time,
clicks_all_time,
cost_all_time,
conversions_all_time,
conversion_value_all_time,
roas_all_time
FROM campaign_ad_groups
WHERE campaign_id = :campaign_id
ORDER BY clicks_30 DESC, clicks_all_time DESC, ad_group_name ASC',
[ ':campaign_id' => (int) $campaign_id ]
) -> fetchAll( \PDO::FETCH_ASSOC );
}
static public function get_campaign_search_terms( $campaign_id, $ad_group_id = 0 )
{
global $mdb;
$sql = 'SELECT
st.id,
st.campaign_id,
st.ad_group_id,
ag.ad_group_name,
st.search_term,
st.impressions_30,
st.clicks_30,
st.cost_30,
st.conversions_30,
st.conversion_value_30,
st.roas_30,
st.impressions_all_time,
st.clicks_all_time,
st.cost_all_time,
st.conversions_all_time,
st.conversion_value_all_time,
st.roas_all_time
FROM campaign_search_terms AS st
LEFT JOIN campaign_ad_groups AS ag ON ag.id = st.ad_group_id
WHERE st.campaign_id = :campaign_id';
$params = [ ':campaign_id' => (int) $campaign_id ];
if ( (int) $ad_group_id > 0 )
{
$sql .= ' AND st.ad_group_id = :ad_group_id';
$params[':ad_group_id'] = (int) $ad_group_id;
}
$sql .= ' ORDER BY st.clicks_30 DESC, st.clicks_all_time DESC, st.search_term ASC';
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
}
static public function get_campaign_negative_keywords( $campaign_id, $ad_group_id = 0 )
{
global $mdb;
$sql = 'SELECT
nk.id,
nk.campaign_id,
nk.ad_group_id,
ag.ad_group_name,
nk.scope,
nk.keyword_text,
nk.match_type
FROM campaign_negative_keywords AS nk
LEFT JOIN campaign_ad_groups AS ag ON ag.id = nk.ad_group_id
WHERE nk.campaign_id = :campaign_id';
$params = [ ':campaign_id' => (int) $campaign_id ];
if ( (int) $ad_group_id > 0 )
{
$sql .= ' AND ( nk.scope = \'campaign\' OR nk.ad_group_id = :ad_group_id )';
$params[':ad_group_id'] = (int) $ad_group_id;
}
$sql .= ' ORDER BY nk.scope ASC, nk.keyword_text ASC';
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
}
static public function get_search_term_context( $search_term_row_id )
{
global $mdb;
return $mdb -> query(
'SELECT
st.id AS search_term_row_id,
st.search_term,
st.campaign_id AS db_campaign_id,
st.ad_group_id AS db_ad_group_id,
c.client_id,
c.campaign_id AS external_campaign_id,
ag.ad_group_id AS external_ad_group_id,
cl.google_ads_customer_id
FROM campaign_search_terms AS st
INNER JOIN campaigns AS c ON c.id = st.campaign_id
INNER JOIN clients AS cl ON cl.id = c.client_id
LEFT JOIN campaign_ad_groups AS ag ON ag.id = st.ad_group_id
WHERE st.id = :search_term_row_id
LIMIT 1',
[ ':search_term_row_id' => (int) $search_term_row_id ]
) -> fetch( \PDO::FETCH_ASSOC );
}
static public function upsert_campaign_negative_keyword( $campaign_id, $ad_group_id, $scope, $keyword_text, $match_type )
{
global $mdb;
$campaign_id = (int) $campaign_id;
$ad_group_id = $ad_group_id !== null ? (int) $ad_group_id : null;
$scope = $scope === 'campaign' ? 'campaign' : 'ad_group';
$keyword_text = trim( (string) $keyword_text );
$match_type = strtoupper( trim( (string) $match_type ) );
if ( $campaign_id <= 0 || $keyword_text === '' )
{
return false;
}
$existing = $mdb -> query(
'SELECT id
FROM campaign_negative_keywords
WHERE campaign_id = :campaign_id
AND ( ( :ad_group_id IS NULL AND ad_group_id IS NULL ) OR ad_group_id = :ad_group_id )
AND scope = :scope
AND LOWER(keyword_text) = LOWER(:keyword_text)
AND UPPER(COALESCE(match_type, \'\')) = :match_type
LIMIT 1',
[
':campaign_id' => $campaign_id,
':ad_group_id' => $ad_group_id,
':scope' => $scope,
':keyword_text' => $keyword_text,
':match_type' => $match_type
]
) -> fetchColumn();
if ( $existing )
{
return (int) $existing;
}
$mdb -> insert( 'campaign_negative_keywords', [
'campaign_id' => $campaign_id,
'ad_group_id' => $ad_group_id,
'scope' => $scope,
'keyword_text' => $keyword_text,
'match_type' => $match_type,
'date_sync' => date( 'Y-m-d' )
] );
return (int) $mdb -> id();
}
static public function delete_campaign( $campaign_id ) static public function delete_campaign( $campaign_id )
{ {
global $mdb; global $mdb;

View File

@@ -180,8 +180,323 @@ class GoogleAdsApi
return $results; return $results;
} }
public function mutate( $customer_id, $mutate_operations, $partial_failure = false )
{
$access_token = $this -> get_access_token();
if ( !$access_token ) return false;
$customer_id = str_replace( '-', '', $customer_id );
$url = self::$ADS_BASE_URL . '/' . self::$API_VERSION
. '/customers/' . $customer_id . '/googleAds:mutate';
$headers = [
'Authorization: Bearer ' . $access_token,
'developer-token: ' . $this -> developer_token,
'Content-Type: application/json',
];
if ( !empty( $this -> manager_account_id ) )
{
$headers[] = 'login-customer-id: ' . str_replace( '-', '', $this -> manager_account_id );
}
$payload = [
'mutateOperations' => array_values( $mutate_operations ),
'partialFailure' => (bool) $partial_failure
];
$ch = curl_init( $url );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode( $payload ),
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 120,
] );
$response = curl_exec( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
$error = curl_error( $ch );
curl_close( $ch );
if ( $http_code !== 200 || !$response )
{
self::set_setting( 'google_ads_last_error', 'mutate failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . substr( (string) $response, 0, 1000 ) );
return false;
}
$data = json_decode( $response, true );
if ( !is_array( $data ) )
{
self::set_setting( 'google_ads_last_error', 'mutate failed: niepoprawna odpowiedz JSON' );
return false;
}
self::set_setting( 'google_ads_last_error', null );
return $data;
}
public function add_negative_keyword_to_ad_group( $customer_id, $ad_group_id, $keyword_text, $match_type = 'PHRASE' )
{
$customer_id = trim( str_replace( '-', '', (string) $customer_id ) );
$ad_group_id = trim( (string) $ad_group_id );
$keyword_text = trim( (string) $keyword_text );
$match_type = strtoupper( trim( (string) $match_type ) );
if ( $customer_id === '' || $ad_group_id === '' || $keyword_text === '' )
{
self::set_setting( 'google_ads_last_error', 'Brak wymaganych danych do dodania frazy wykluczajacej.' );
return [ 'success' => false, 'duplicate' => false ];
}
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
{
$match_type = 'PHRASE';
}
$operation = [
'adGroupCriterionOperation' => [
'create' => [
'adGroup' => 'customers/' . $customer_id . '/adGroups/' . $ad_group_id,
'negative' => true,
'keyword' => [
'text' => $keyword_text,
'matchType' => $match_type
]
]
]
];
$result = $this -> mutate( $customer_id, [ $operation ] );
if ( $result === false )
{
$last_error = (string) self::get_setting( 'google_ads_last_error' );
$is_duplicate = stripos( $last_error, 'DUPLICATE' ) !== false
|| stripos( $last_error, 'already exists' ) !== false;
if ( $is_duplicate )
{
return [
'success' => true,
'duplicate' => true,
'verification' => $this -> verify_negative_keyword_exists( $customer_id, 'ad_group', $keyword_text, $match_type, null, $ad_group_id )
];
}
return [ 'success' => false, 'duplicate' => false, 'sent_operation' => $operation ];
}
return [
'success' => true,
'duplicate' => false,
'response' => $result,
'sent_operation' => $operation,
'verification' => $this -> verify_negative_keyword_exists( $customer_id, 'ad_group', $keyword_text, $match_type, null, $ad_group_id )
];
}
public function add_negative_keyword_to_campaign( $customer_id, $campaign_id, $keyword_text, $match_type = 'PHRASE' )
{
$customer_id = trim( str_replace( '-', '', (string) $customer_id ) );
$campaign_id = trim( (string) $campaign_id );
$keyword_text = trim( (string) $keyword_text );
$match_type = strtoupper( trim( (string) $match_type ) );
if ( $customer_id === '' || $campaign_id === '' || $keyword_text === '' )
{
self::set_setting( 'google_ads_last_error', 'Brak wymaganych danych do dodania frazy wykluczajacej.' );
return [ 'success' => false, 'duplicate' => false ];
}
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
{
$match_type = 'PHRASE';
}
$operation = [
'campaignCriterionOperation' => [
'create' => [
'campaign' => 'customers/' . $customer_id . '/campaigns/' . $campaign_id,
'negative' => true,
'keyword' => [
'text' => $keyword_text,
'matchType' => $match_type
]
]
]
];
$result = $this -> mutate( $customer_id, [ $operation ] );
if ( $result === false )
{
$last_error = (string) self::get_setting( 'google_ads_last_error' );
$is_duplicate = stripos( $last_error, 'DUPLICATE' ) !== false
|| stripos( $last_error, 'already exists' ) !== false;
if ( $is_duplicate )
{
return [
'success' => true,
'duplicate' => true,
'verification' => $this -> verify_negative_keyword_exists( $customer_id, 'campaign', $keyword_text, $match_type, $campaign_id, null )
];
}
return [ 'success' => false, 'duplicate' => false, 'sent_operation' => $operation ];
}
return [
'success' => true,
'duplicate' => false,
'response' => $result,
'sent_operation' => $operation,
'verification' => $this -> verify_negative_keyword_exists( $customer_id, 'campaign', $keyword_text, $match_type, $campaign_id, null )
];
}
private function gaql_escape( $value )
{
return str_replace( [ '\\', '\'' ], [ '\\\\', '\\\'' ], (string) $value );
}
private function verify_negative_keyword_exists( $customer_id, $scope, $keyword_text, $match_type, $campaign_id = null, $ad_group_id = null )
{
$customer_id = trim( str_replace( '-', '', (string) $customer_id ) );
$scope = $scope === 'campaign' ? 'campaign' : 'ad_group';
$match_type = strtoupper( trim( (string) $match_type ) );
$keyword_text_escaped = $this -> gaql_escape( trim( (string) $keyword_text ) );
if ( $scope === 'campaign' )
{
$campaign_id = trim( (string) $campaign_id );
if ( $campaign_id === '' || $keyword_text_escaped === '' )
{
return [ 'found' => false, 'scope' => $scope, 'rows' => [], 'error' => 'Brak danych do weryfikacji.' ];
}
$gaql = "SELECT "
. "campaign_criterion.resource_name, "
. "campaign.id, "
. "campaign_criterion.keyword.text, "
. "campaign_criterion.keyword.match_type "
. "FROM campaign_criterion "
. "WHERE campaign.id = " . $campaign_id . " "
. "AND campaign_criterion.type = 'KEYWORD' "
. "AND campaign_criterion.negative = TRUE "
. "AND campaign_criterion.keyword.text = '" . $keyword_text_escaped . "' "
. "AND campaign_criterion.keyword.match_type = " . $match_type . " "
. "LIMIT 5";
}
else
{
$ad_group_id = trim( (string) $ad_group_id );
if ( $ad_group_id === '' || $keyword_text_escaped === '' )
{
return [ 'found' => false, 'scope' => $scope, 'rows' => [], 'error' => 'Brak danych do weryfikacji.' ];
}
$gaql = "SELECT "
. "ad_group_criterion.resource_name, "
. "campaign.id, "
. "ad_group.id, "
. "ad_group_criterion.keyword.text, "
. "ad_group_criterion.keyword.match_type "
. "FROM ad_group_criterion "
. "WHERE ad_group.id = " . $ad_group_id . " "
. "AND ad_group_criterion.type = 'KEYWORD' "
. "AND ad_group_criterion.negative = TRUE "
. "AND ad_group_criterion.keyword.text = '" . $keyword_text_escaped . "' "
. "AND ad_group_criterion.keyword.match_type = " . $match_type . " "
. "LIMIT 5";
}
$rows = [];
$last_error = null;
for ( $i = 0; $i < 3; $i++ )
{
$result = $this -> search_stream( $customer_id, $gaql );
if ( is_array( $result ) )
{
$rows = $result;
if ( count( $rows ) > 0 )
{
return [ 'found' => true, 'scope' => $scope, 'rows' => $rows ];
}
}
else
{
$last_error = (string) self::get_setting( 'google_ads_last_error' );
}
usleep( 400000 );
}
return [
'found' => count( $rows ) > 0,
'scope' => $scope,
'rows' => $rows,
'error' => $last_error
];
}
// --- Kampanie: dane 30-dniowe --- // --- Kampanie: dane 30-dniowe ---
public function get_products_for_date( $customer_id, $date )
{
$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 . "'";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
$products = [];
foreach ( $results as $row )
{
$offer_id = trim( (string) ( $row['segments']['productItemId'] ?? '' ) );
if ( $offer_id === '' )
{
continue;
}
if ( !isset( $products[ $offer_id ] ) )
{
$products[ $offer_id ] = [
'OfferId' => $offer_id,
'ProductTitle' => (string) ( $row['segments']['productTitle'] ?? $offer_id ),
'Impressions' => 0,
'Clicks' => 0,
'Cost' => 0.0,
'Conversions' => 0.0,
'ConversionValue' => 0.0
];
}
$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 );
}
return array_values( $products );
}
public function get_campaigns_30_days( $customer_id ) public function get_campaigns_30_days( $customer_id )
{ {
$gaql = "SELECT " $gaql = "SELECT "
@@ -264,11 +579,296 @@ class GoogleAdsApi
$value = (float) ( $row['metrics']['conversionsValue'] ?? 0 ); $value = (float) ( $row['metrics']['conversionsValue'] ?? 0 );
$campaigns[] = [ $campaigns[] = [
'campaign_id' => $cid, 'campaign_id' => $cid,
'roas_all_time' => ( $cost > 0 ) ? round( ( $value / $cost ) * 100, 2 ) : 0, 'cost_all_time' => $cost,
'conversion_value_all_time' => $value,
'roas_all_time' => ( $cost > 0 ) ? round( ( $value / $cost ) * 100, 2 ) : 0,
]; ];
} }
return $campaigns; return $campaigns;
} }
public function get_ad_groups_30_days( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "ad_group.id, "
. "ad_group.name, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM ad_group "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "AND segments.date DURING LAST_30_DAYS";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
return $this -> aggregate_ad_groups( $results );
}
public function get_ad_groups_all_time( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "ad_group.id, "
. "ad_group.name, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM ad_group "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED'";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
return $this -> aggregate_ad_groups( $results );
}
public function get_search_terms_30_days( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "ad_group.id, "
. "ad_group.name, "
. "search_term_view.search_term, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM search_term_view "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "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_search_terms( $results );
}
public function get_search_terms_all_time( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "ad_group.id, "
. "ad_group.name, "
. "search_term_view.search_term, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM search_term_view "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "AND metrics.clicks > 0";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
return $this -> aggregate_search_terms( $results );
}
public function get_negative_keywords( $customer_id )
{
$campaign_gaql = "SELECT "
. "campaign.id, "
. "campaign_criterion.keyword.text, "
. "campaign_criterion.keyword.match_type "
. "FROM campaign_criterion "
. "WHERE campaign.status != 'REMOVED' "
. "AND campaign_criterion.type = 'KEYWORD' "
. "AND campaign_criterion.negative = TRUE";
$ad_group_gaql = "SELECT "
. "campaign.id, "
. "ad_group.id, "
. "ad_group_criterion.keyword.text, "
. "ad_group_criterion.keyword.match_type "
. "FROM ad_group_criterion "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "AND ad_group_criterion.type = 'KEYWORD' "
. "AND ad_group_criterion.negative = TRUE";
$campaign_results = $this -> search_stream( $customer_id, $campaign_gaql );
if ( $campaign_results === false ) return false;
$ad_group_results = $this -> search_stream( $customer_id, $ad_group_gaql );
if ( $ad_group_results === false ) return false;
$negatives = [];
$seen = [];
foreach ( $campaign_results as $row )
{
$campaign_id = $row['campaign']['id'] ?? null;
$text = trim( (string) ( $row['campaignCriterion']['keyword']['text'] ?? '' ) );
$match_type = (string) ( $row['campaignCriterion']['keyword']['matchType'] ?? '' );
if ( !$campaign_id || $text === '' )
{
continue;
}
$key = 'campaign|' . $campaign_id . '||' . strtolower( $text ) . '|' . $match_type;
if ( isset( $seen[ $key ] ) )
{
continue;
}
$seen[ $key ] = true;
$negatives[] = [
'scope' => 'campaign',
'campaign_id' => (int) $campaign_id,
'ad_group_id' => null,
'keyword_text' => $text,
'match_type' => $match_type
];
}
foreach ( $ad_group_results as $row )
{
$campaign_id = $row['campaign']['id'] ?? null;
$ad_group_id = $row['adGroup']['id'] ?? null;
$text = trim( (string) ( $row['adGroupCriterion']['keyword']['text'] ?? '' ) );
$match_type = (string) ( $row['adGroupCriterion']['keyword']['matchType'] ?? '' );
if ( !$campaign_id || !$ad_group_id || $text === '' )
{
continue;
}
$key = 'ad_group|' . $campaign_id . '|' . $ad_group_id . '|' . strtolower( $text ) . '|' . $match_type;
if ( isset( $seen[ $key ] ) )
{
continue;
}
$seen[ $key ] = true;
$negatives[] = [
'scope' => 'ad_group',
'campaign_id' => (int) $campaign_id,
'ad_group_id' => (int) $ad_group_id,
'keyword_text' => $text,
'match_type' => $match_type
];
}
return $negatives;
}
private function aggregate_ad_groups( $results )
{
$ad_groups = [];
foreach ( $results as $row )
{
$campaign_id = $row['campaign']['id'] ?? null;
$ad_group_id = $row['adGroup']['id'] ?? null;
if ( !$campaign_id || !$ad_group_id )
{
continue;
}
$key = $campaign_id . '|' . $ad_group_id;
if ( !isset( $ad_groups[ $key ] ) )
{
$ad_groups[ $key ] = [
'campaign_id' => (int) $campaign_id,
'ad_group_id' => (int) $ad_group_id,
'ad_group_name' => (string) ( $row['adGroup']['name'] ?? '' ),
'impressions' => 0,
'clicks' => 0,
'cost' => 0.0,
'conversions' => 0.0,
'conversion_value' => 0.0,
'roas' => 0.0
];
}
$ad_groups[ $key ]['impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
$ad_groups[ $key ]['clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
$ad_groups[ $key ]['cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
$ad_groups[ $key ]['conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
$ad_groups[ $key ]['conversion_value'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
}
foreach ( $ad_groups as &$ad_group )
{
$ad_group['roas'] = ( $ad_group['cost'] > 0 )
? round( ( $ad_group['conversion_value'] / $ad_group['cost'] ) * 100, 2 )
: 0;
}
return array_values( $ad_groups );
}
private function aggregate_search_terms( $results )
{
$terms = [];
foreach ( $results as $row )
{
$campaign_id = $row['campaign']['id'] ?? null;
$ad_group_id = $row['adGroup']['id'] ?? null;
$search_term = trim( (string) ( $row['searchTermView']['searchTerm'] ?? '' ) );
if ( !$campaign_id || !$ad_group_id || $search_term === '' )
{
continue;
}
$key = $campaign_id . '|' . $ad_group_id . '|' . strtolower( $search_term );
if ( !isset( $terms[ $key ] ) )
{
$terms[ $key ] = [
'campaign_id' => (int) $campaign_id,
'ad_group_id' => (int) $ad_group_id,
'ad_group_name' => (string) ( $row['adGroup']['name'] ?? '' ),
'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 );
}
} }

344
docs/database.sql Normal file
View File

@@ -0,0 +1,344 @@
-- --------------------------------------------------------
-- Host: host700513.hostido.net.pl
-- Wersja serwera: 10.11.15-MariaDB-cll-lve - MariaDB Server
-- Serwer OS: Linux
-- HeidiSQL Wersja: 12.6.0.6765
-- --------------------------------------------------------
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
-- Zrzut struktury tabela host700513_adspro.campaigns
CREATE TABLE IF NOT EXISTS `campaigns` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`client_id` int(11) NOT NULL DEFAULT 0,
`campaign_id` bigint(20) NOT NULL DEFAULT 0,
`campaign_name` varchar(255) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `client_id` (`client_id`),
CONSTRAINT `FK__clients` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=123 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.campaigns_comments
CREATE TABLE IF NOT EXISTS `campaigns_comments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`campaign_id` int(11) NOT NULL,
`comment` text NOT NULL,
`date_add` date NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`) USING BTREE,
KEY `campaign_id` (`campaign_id`),
CONSTRAINT `FK_campaigns_comments_campaigns` FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.campaigns_history
CREATE TABLE IF NOT EXISTS `campaigns_history` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`campaign_id` int(11) NOT NULL DEFAULT 0,
`roas_30_days` decimal(20,6) NOT NULL DEFAULT 0.000000,
`roas_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`budget` decimal(20,6) NOT NULL DEFAULT 0.000000,
`money_spent` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value` decimal(20,6) NOT NULL DEFAULT 0.000000,
`bidding_strategy` text DEFAULT NULL,
`date_add` date NOT NULL DEFAULT '0000-00-00',
PRIMARY KEY (`id`) USING BTREE,
KEY `offer_id` (`campaign_id`) USING BTREE,
CONSTRAINT `FK_campaigns_history_campaigns` FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=4400 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.clients
CREATE TABLE IF NOT EXISTS `clients` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '0',
`google_ads_customer_id` varchar(20) DEFAULT NULL,
`google_ads_start_date` date DEFAULT NULL,
`deleted` int(11) DEFAULT 0,
`bestseller_min_roas` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=46 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.phrases
CREATE TABLE IF NOT EXISTS `phrases` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`client_id` int(11) NOT NULL DEFAULT 0,
`phrase` varchar(255) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `FK_phrases_clients` (`client_id`),
CONSTRAINT `FK_phrases_clients` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=5512 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.phrases_history
CREATE TABLE IF NOT EXISTS `phrases_history` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`phrase_id` int(11) NOT NULL DEFAULT 0,
`impressions` int(11) NOT NULL DEFAULT 0,
`clicks` int(11) NOT NULL DEFAULT 0,
`cost` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_value` decimal(20,6) NOT NULL DEFAULT 0.000000,
`date_add` date NOT NULL DEFAULT '0000-00-00',
`updated` int(11) NOT NULL DEFAULT 0,
`deleted` int(11) DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE,
KEY `offer_id` (`phrase_id`) USING BTREE,
CONSTRAINT `FK_phrases_history_phrases` FOREIGN KEY (`phrase_id`) REFERENCES `phrases` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=13088 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.phrases_history_30
CREATE TABLE IF NOT EXISTS `phrases_history_30` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`phrase_id` int(11) NOT NULL,
`impressions` int(11) NOT NULL,
`clicks` int(11) NOT NULL,
`cost` decimal(20,6) NOT NULL,
`conversions` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_value` decimal(20,6) NOT NULL,
`roas` decimal(20,6) NOT NULL,
`date_add` date NOT NULL DEFAULT '0000-00-00',
`deleted` int(11) DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE,
KEY `offer_id` (`phrase_id`) USING BTREE,
CONSTRAINT `FK_phrases_history_30_phrases` FOREIGN KEY (`phrase_id`) REFERENCES `phrases` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1795 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.phrases_temp
CREATE TABLE IF NOT EXISTS `phrases_temp` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`phrase_id` int(11) DEFAULT NULL,
`phrase` varchar(255) DEFAULT NULL,
`impressions` int(11) DEFAULT NULL,
`clicks` int(11) DEFAULT NULL,
`cost` decimal(20,6) DEFAULT NULL,
`conversions` decimal(20,6) DEFAULT NULL,
`conversions_value` decimal(20,6) DEFAULT NULL,
`cpc` decimal(20,6) DEFAULT NULL,
`roas` decimal(20,0) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
KEY `offer_id` (`phrase_id`) USING BTREE,
CONSTRAINT `FK_phrases_temp_phrases` FOREIGN KEY (`phrase_id`) REFERENCES `phrases` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=353973 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.products
CREATE TABLE IF NOT EXISTS `products` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`client_id` int(11) NOT NULL DEFAULT 0,
`offer_id` varchar(50) NOT NULL DEFAULT '0',
`name` varchar(255) NOT NULL DEFAULT '0',
`min_roas` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `FK_offers_clients` (`client_id`),
CONSTRAINT `FK_offers_clients` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=5927 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.products_comments
CREATE TABLE IF NOT EXISTS `products_comments` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NOT NULL,
`comment` text NOT NULL,
`date_add` date NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
KEY `product_id` (`product_id`) USING BTREE,
CONSTRAINT `FK_products_comments_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=118 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.products_data
CREATE TABLE IF NOT EXISTS `products_data` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) DEFAULT NULL,
`custom_label_4` varchar(255) DEFAULT NULL,
`custom_label_3` varchar(255) DEFAULT NULL,
`title` varchar(255) DEFAULT NULL,
`description` text DEFAULT NULL,
`google_product_category` text DEFAULT NULL,
`product_url` varchar(500) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `product_id` (`product_id`) USING BTREE,
CONSTRAINT `FK_products_data_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=118 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.products_history
CREATE TABLE IF NOT EXISTS `products_history` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NOT NULL DEFAULT 0,
`impressions` int(11) NOT NULL DEFAULT 0,
`clicks` int(11) NOT NULL DEFAULT 0,
`ctr` decimal(20,6) NOT NULL DEFAULT 0.000000,
`cost` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_value` decimal(20,6) NOT NULL DEFAULT 0.000000,
`date_add` date NOT NULL DEFAULT '0000-00-00',
`updated` int(11) NOT NULL DEFAULT 0,
`deleted` int(11) DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE,
KEY `product_id` (`product_id`) USING BTREE,
CONSTRAINT `FK_products_history_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=63549 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.products_history_30
CREATE TABLE IF NOT EXISTS `products_history_30` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) NOT NULL,
`impressions` int(11) NOT NULL,
`clicks` int(11) NOT NULL,
`ctr` decimal(20,6) NOT NULL DEFAULT 0.000000,
`cost` decimal(20,6) NOT NULL,
`conversions` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_value` decimal(20,6) NOT NULL,
`roas` decimal(20,6) NOT NULL,
`roas_all_time` decimal(20,6) NOT NULL,
`date_add` date NOT NULL DEFAULT '0000-00-00',
`deleted` int(11) DEFAULT 0,
PRIMARY KEY (`id`),
KEY `product_id` (`product_id`) USING BTREE,
CONSTRAINT `FK_products_history_30_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=27655 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.products_temp
CREATE TABLE IF NOT EXISTS `products_temp` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`product_id` int(11) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`impressions` int(11) DEFAULT NULL,
`impressions_30` int(11) DEFAULT NULL,
`clicks` int(11) DEFAULT NULL,
`clicks_30` int(11) DEFAULT NULL,
`ctr` decimal(20,6) DEFAULT NULL,
`cost` decimal(20,6) DEFAULT NULL,
`conversions` decimal(20,6) DEFAULT NULL,
`conversions_value` decimal(20,6) DEFAULT NULL,
`cpc` decimal(20,6) DEFAULT NULL,
`roas` decimal(20,0) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `product_id` (`product_id`) USING BTREE,
CONSTRAINT `FK_products_temp_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=298845 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.settings
CREATE TABLE IF NOT EXISTS `settings` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`setting_key` varchar(100) NOT NULL,
`setting_value` text DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_setting_key` (`setting_key`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
-- Eksport danych został odznaczony.
-- Zrzut struktury tabela host700513_adspro.users
CREATE TABLE IF NOT EXISTS `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`email` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`surname` varchar(255) DEFAULT NULL,
`default_project` int(11) DEFAULT NULL,
`color` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_polish_ci;
-- Eksport danych został odznaczony.
/*!40103 SET TIME_ZONE=IFNULL(@OLD_TIME_ZONE, 'system') */;
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */;
-- ================================
-- DODANE: struktury kampanie > grupy/frazy
-- ================================
CREATE TABLE IF NOT EXISTS `campaign_ad_groups` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`campaign_id` int(11) NOT NULL,
`ad_group_id` bigint(20) NOT NULL,
`ad_group_name` varchar(255) NOT NULL DEFAULT '',
`impressions_30` int(11) NOT NULL DEFAULT 0,
`clicks_30` int(11) NOT NULL DEFAULT 0,
`cost_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`roas_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`impressions_all_time` int(11) NOT NULL DEFAULT 0,
`clicks_all_time` int(11) NOT NULL DEFAULT 0,
`cost_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`roas_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`date_sync` date DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_campaign_ad_groups_campaign_ad_group` (`campaign_id`,`ad_group_id`),
KEY `idx_campaign_ad_groups_campaign_id` (`campaign_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `campaign_search_terms` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`campaign_id` int(11) NOT NULL,
`ad_group_id` int(11) NOT NULL,
`search_term` varchar(255) NOT NULL,
`impressions_30` int(11) NOT NULL DEFAULT 0,
`clicks_30` int(11) NOT NULL DEFAULT 0,
`cost_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`roas_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
`impressions_all_time` int(11) NOT NULL DEFAULT 0,
`clicks_all_time` int(11) NOT NULL DEFAULT 0,
`cost_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversions_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`roas_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
`date_sync` date DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_campaign_search_terms` (`campaign_id`,`ad_group_id`,`search_term`),
KEY `idx_campaign_search_terms_campaign_id` (`campaign_id`),
KEY `idx_campaign_search_terms_ad_group_id` (`ad_group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `campaign_negative_keywords` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`campaign_id` int(11) NOT NULL,
`ad_group_id` int(11) DEFAULT NULL,
`scope` varchar(20) NOT NULL DEFAULT 'campaign',
`keyword_text` varchar(255) NOT NULL,
`match_type` varchar(40) DEFAULT NULL,
`date_sync` date DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_campaign_negative_keywords_campaign_id` (`campaign_id`),
KEY `idx_campaign_negative_keywords_ad_group_id` (`ad_group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

195
install.php Normal file
View File

@@ -0,0 +1,195 @@
<?php
error_reporting( E_ALL );
date_default_timezone_set( 'Europe/Warsaw' );
if ( PHP_SAPI !== 'cli' )
{
header( 'Content-Type: text/plain; charset=utf-8' );
}
require_once __DIR__ . '/config.php';
function has_flag( $name )
{
if ( PHP_SAPI === 'cli' )
{
global $argv;
return in_array( '--' . $name, $argv, true );
}
return isset( $_GET[ $name ] ) && (string) $_GET[ $name ] === '1';
}
function statement_complete( $buffer, $delimiter )
{
$trimmed = rtrim( $buffer );
if ( $trimmed === '' )
{
return false;
}
$dl = strlen( $delimiter );
if ( $dl === 0 )
{
return false;
}
return substr( $trimmed, -$dl ) === $delimiter;
}
function strip_statement_delimiter( $buffer, $delimiter )
{
$trimmed = rtrim( $buffer );
$dl = strlen( $delimiter );
if ( $dl > 0 && substr( $trimmed, -$dl ) === $delimiter )
{
return rtrim( substr( $trimmed, 0, -$dl ) );
}
return $trimmed;
}
function execute_sql_file( PDO $pdo, $file_path )
{
$handle = fopen( $file_path, 'r' );
if ( !$handle )
{
throw new RuntimeException( 'Nie mozna otworzyc pliku: ' . $file_path );
}
$delimiter = ';';
$buffer = '';
while ( ( $line = fgets( $handle ) ) !== false )
{
if ( preg_match( '/^\s*DELIMITER\s+(\S+)\s*$/i', $line, $m ) )
{
$delimiter = $m[1];
continue;
}
$trim = trim( $line );
if ( $trim !== '' && preg_match( '/^\s*(--|#)/', $trim ) )
{
continue;
}
$buffer .= $line;
if ( statement_complete( $buffer, $delimiter ) )
{
$statement = strip_statement_delimiter( $buffer, $delimiter );
$statement = trim( $statement );
if ( $statement !== '' )
{
$pdo -> exec( $statement );
}
$buffer = '';
}
}
fclose( $handle );
$tail = trim( $buffer );
if ( $tail !== '' )
{
$pdo -> exec( $tail );
}
}
try
{
$dsn = 'mysql:host=' . $database['host'] . ';dbname=' . $database['name'] . ';charset=utf8';
$pdo = new PDO( $dsn, $database['user'], $database['password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
] );
$pdo -> exec(
'CREATE TABLE IF NOT EXISTS `schema_migrations` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`filename` VARCHAR(255) NOT NULL,
`applied_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_schema_migrations_filename` (`filename`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8'
);
$include_demo = has_flag( 'with_demo' );
$force = has_flag( 'force' );
$files = glob( __DIR__ . '/migrations/*.sql' );
sort( $files, SORT_NATURAL | SORT_FLAG_CASE );
$selected = [];
foreach ( $files as $file_path )
{
$filename = basename( $file_path );
if ( preg_match( '/^\d+_.+\.sql$/', $filename ) )
{
$selected[] = $file_path;
continue;
}
if ( $include_demo && $filename === 'demo_data.sql' )
{
$selected[] = $file_path;
}
}
if ( empty( $selected ) )
{
echo "Brak migracji do uruchomienia.\n";
exit;
}
echo "Uruchamianie migracji...\n";
echo 'Tryb force: ' . ( $force ? 'TAK' : 'NIE' ) . "\n";
echo 'Demo data: ' . ( $include_demo ? 'TAK' : 'NIE' ) . "\n\n";
$applied_count = 0;
$skipped_count = 0;
foreach ( $selected as $file_path )
{
$filename = basename( $file_path );
$stmt = $pdo -> prepare( 'SELECT COUNT(1) FROM schema_migrations WHERE filename = :filename' );
$stmt -> execute( [ ':filename' => $filename ] );
$already_applied = (int) $stmt -> fetchColumn() > 0;
if ( $already_applied && !$force )
{
echo '[SKIP] ' . $filename . " (juz zastosowana)\n";
$skipped_count++;
continue;
}
execute_sql_file( $pdo, $file_path );
$stmt = $pdo -> prepare(
'INSERT INTO schema_migrations (filename, applied_at)
VALUES (:filename, NOW())
ON DUPLICATE KEY UPDATE applied_at = NOW()'
);
$stmt -> execute( [ ':filename' => $filename ] );
echo '[OK] ' . $filename . "\n";
$applied_count++;
}
echo "\nZakonczono.\n";
echo 'Zastosowano: ' . $applied_count . "\n";
echo 'Pominieto: ' . $skipped_count . "\n";
}
catch ( Throwable $e )
{
http_response_code( 500 );
echo '[BLAD] ' . $e -> getMessage() . "\n";
exit( 1 );
}

View File

@@ -1595,12 +1595,14 @@ table#products a.custom_name {
color: #718096; color: #718096;
} }
.jconfirm-box .form-group .select2-container { .jconfirm-box .form-group .select2-container,
.adspro-dialog-box .form-group .select2-container {
width: 100% !important; width: 100% !important;
margin-top: 8px; margin-top: 8px;
} }
.jconfirm-box .select2-container--default .select2-selection--single { .jconfirm-box .select2-container--default .select2-selection--single,
.adspro-dialog-box .select2-container--default .select2-selection--single {
background-color: #FFFFFF; background-color: #FFFFFF;
border: 1px solid #E2E8F0; border: 1px solid #E2E8F0;
border-radius: 6px; border-radius: 6px;
@@ -1613,42 +1615,50 @@ table#products a.custom_name {
font-size: 14px; font-size: 14px;
} }
.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__rendered { .jconfirm-box .select2-container--default .select2-selection--single .select2-selection__rendered,
.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__rendered {
padding-left: 0; padding-left: 0;
line-height: 1.4; line-height: 1.4;
color: #495057; color: #495057;
} }
.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__placeholder { .jconfirm-box .select2-container--default .select2-selection--single .select2-selection__placeholder,
.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__placeholder {
color: #CBD5E0; color: #CBD5E0;
} }
.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__arrow { .jconfirm-box .select2-container--default .select2-selection--single .select2-selection__arrow,
.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__arrow {
height: 100%; height: 100%;
right: 8px; right: 8px;
} }
.jconfirm-box .select2-container--default.select2-container--focus .select2-selection--single, .jconfirm-box .select2-container--default.select2-container--focus .select2-selection--single,
.jconfirm-box .select2-container--default .select2-selection--single:hover { .jconfirm-box .select2-container--default .select2-selection--single:hover,
.adspro-dialog-box .select2-container--default.select2-container--focus .select2-selection--single,
.adspro-dialog-box .select2-container--default .select2-selection--single:hover {
border-color: #6690F4; border-color: #6690F4;
box-shadow: 0 0 0 3px rgba(102, 144, 244, 0.1); box-shadow: 0 0 0 3px rgba(102, 144, 244, 0.1);
outline: 0; outline: 0;
} }
.jconfirm-box .select2-container .select2-dropdown { .jconfirm-box .select2-container .select2-dropdown,
.adspro-dialog-box .select2-container .select2-dropdown {
border-color: #E2E8F0; border-color: #E2E8F0;
border-radius: 0 0 6px 6px; border-radius: 0 0 6px 6px;
font-size: 14px; font-size: 14px;
} }
.jconfirm-box .select2-container .select2-search--dropdown .select2-search__field { .jconfirm-box .select2-container .select2-search--dropdown .select2-search__field,
.adspro-dialog-box .select2-container .select2-search--dropdown .select2-search__field {
padding: 6px 10px; padding: 6px 10px;
border-radius: 4px; border-radius: 4px;
border: 1px solid #E2E8F0; border: 1px solid #E2E8F0;
font-size: 14px; font-size: 14px;
} }
.jconfirm-box .select2-container--default .select2-results__option--highlighted[aria-selected] { .jconfirm-box .select2-container--default .select2-results__option--highlighted[aria-selected],
.adspro-dialog-box .select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: #6690F4; background-color: #6690F4;
color: #FFFFFF; color: #FFFFFF;
} }
@@ -1664,3 +1674,5 @@ table#products a.custom_name {
margin-left: 0 !important; margin-left: 0 !important;
} }
} }
/*# sourceMappingURL=style.css.map */

File diff suppressed because one or more lines are too long

View File

@@ -1906,12 +1906,14 @@ table#products {
} }
// --- Select2 w modalu --- // --- Select2 w modalu ---
.jconfirm-box .form-group .select2-container { .jconfirm-box .form-group .select2-container,
.adspro-dialog-box .form-group .select2-container {
width: 100% !important; width: 100% !important;
margin-top: 8px; margin-top: 8px;
} }
.jconfirm-box .select2-container--default .select2-selection--single { .jconfirm-box .select2-container--default .select2-selection--single,
.adspro-dialog-box .select2-container--default .select2-selection--single {
background-color: $cWhite; background-color: $cWhite;
border: 1px solid $cBorder; border: 1px solid $cBorder;
border-radius: 6px; border-radius: 6px;
@@ -1924,42 +1926,50 @@ table#products {
font-size: 14px; font-size: 14px;
} }
.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__rendered { .jconfirm-box .select2-container--default .select2-selection--single .select2-selection__rendered,
.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__rendered {
padding-left: 0; padding-left: 0;
line-height: 1.4; line-height: 1.4;
color: #495057; color: #495057;
} }
.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__placeholder { .jconfirm-box .select2-container--default .select2-selection--single .select2-selection__placeholder,
.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__placeholder {
color: #CBD5E0; color: #CBD5E0;
} }
.jconfirm-box .select2-container--default .select2-selection--single .select2-selection__arrow { .jconfirm-box .select2-container--default .select2-selection--single .select2-selection__arrow,
.adspro-dialog-box .select2-container--default .select2-selection--single .select2-selection__arrow {
height: 100%; height: 100%;
right: 8px; right: 8px;
} }
.jconfirm-box .select2-container--default.select2-container--focus .select2-selection--single, .jconfirm-box .select2-container--default.select2-container--focus .select2-selection--single,
.jconfirm-box .select2-container--default .select2-selection--single:hover { .jconfirm-box .select2-container--default .select2-selection--single:hover,
.adspro-dialog-box .select2-container--default.select2-container--focus .select2-selection--single,
.adspro-dialog-box .select2-container--default .select2-selection--single:hover {
border-color: $cPrimary; border-color: $cPrimary;
box-shadow: 0 0 0 3px rgba($cPrimary, 0.1); box-shadow: 0 0 0 3px rgba($cPrimary, 0.1);
outline: 0; outline: 0;
} }
.jconfirm-box .select2-container .select2-dropdown { .jconfirm-box .select2-container .select2-dropdown,
.adspro-dialog-box .select2-container .select2-dropdown {
border-color: $cBorder; border-color: $cBorder;
border-radius: 0 0 6px 6px; border-radius: 0 0 6px 6px;
font-size: 14px; font-size: 14px;
} }
.jconfirm-box .select2-container .select2-search--dropdown .select2-search__field { .jconfirm-box .select2-container .select2-search--dropdown .select2-search__field,
.adspro-dialog-box .select2-container .select2-search--dropdown .select2-search__field {
padding: 6px 10px; padding: 6px 10px;
border-radius: 4px; border-radius: 4px;
border: 1px solid $cBorder; border: 1px solid $cBorder;
font-size: 14px; font-size: 14px;
} }
.jconfirm-box .select2-container--default .select2-results__option--highlighted[aria-selected] { .jconfirm-box .select2-container--default .select2-results__option--highlighted[aria-selected],
.adspro-dialog-box .select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: $cPrimary; background-color: $cPrimary;
color: $cWhite; color: $cWhite;
} }

292
libraries/adspro-dialog.css Normal file
View File

@@ -0,0 +1,292 @@
/* =============================================================
AdsProDialog - Custom Dialog System
============================================================= */
/* --- Animacje --- */
@keyframes adspro-dialog-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* --- Główny wrapper --- */
.adspro-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
opacity: 0;
transition: opacity 0.25s ease;
}
.adspro-dialog.adspro-dialog-open {
opacity: 1;
}
.adspro-dialog.adspro-dialog-closing {
opacity: 0;
pointer-events: none;
}
/* --- Backdrop --- */
.adspro-dialog-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
}
/* --- Scrollpane --- */
.adspro-dialog-scrollpane {
position: relative;
width: 100%;
height: 100%;
overflow-y: auto;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 40px 20px;
}
/* --- Center container --- */
.adspro-dialog-center {
display: flex;
justify-content: center;
width: 100%;
min-height: 0;
margin: auto 0;
}
/* Wsparcie columnClass (Bootstrap grid) */
.adspro-dialog-center.col-md-2 { max-width: 16.666%; }
.adspro-dialog-center.col-md-3 { max-width: 25%; }
.adspro-dialog-center.col-md-4 { max-width: 33.333%; }
.adspro-dialog-center.col-md-5 { max-width: 41.666%; }
.adspro-dialog-center.col-md-6 { max-width: 50%; }
.adspro-dialog-center.col-md-7 { max-width: 58.333%; }
.adspro-dialog-center.col-md-8 { max-width: 66.666%; }
.adspro-dialog-center.col-md-9 { max-width: 75%; }
.adspro-dialog-center.col-md-10 { max-width: 83.333%; }
.adspro-dialog-center.col-md-11 { max-width: 91.666%; }
.adspro-dialog-center.col-md-12 { max-width: 100%; }
.adspro-dialog-center.col-12 { max-width: 100%; }
/* --- Dialog box --- */
.adspro-dialog-box {
background: #FFFFFF;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2), 0 4px 16px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 550px;
position: relative;
overflow: hidden;
transform: scale(0.95) translateY(-20px);
opacity: 0;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.adspro-dialog-open .adspro-dialog-box {
transform: scale(1) translateY(0);
opacity: 1;
}
.adspro-dialog-closing .adspro-dialog-box {
transform: scale(0.95) translateY(-10px);
opacity: 0;
}
/* Gdy użyto columnClass lub boxWidth, box zajmuje pełną szerokość kontenera */
.adspro-dialog-center[class*="col-"] .adspro-dialog-box {
max-width: 100%;
}
.adspro-dialog-box[style*="max-width"] {
max-width: none;
}
/* --- Kolorowe paski (type) --- */
.adspro-dialog-type-red { border-top: 4px solid #CC0000; }
.adspro-dialog-type-orange { border-top: 4px solid #FF8C00; }
.adspro-dialog-type-green { border-top: 4px solid #57B951; }
.adspro-dialog-type-blue { border-top: 4px solid #6690F4; }
.adspro-dialog-type-purple { border-top: 4px solid #8B5CF6; }
.adspro-dialog-type-dark { border-top: 4px solid #2D3748; }
/* --- Close icon --- */
.adspro-dialog-close-icon {
position: absolute;
top: 14px;
right: 14px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #A0AEC0;
border-radius: 6px;
transition: all 0.2s;
z-index: 10;
font-size: 16px;
}
.adspro-dialog-close-icon:hover {
background: #FFF5F5;
color: #CC0000;
}
/* --- Tytuł --- */
.adspro-dialog-title-c {
padding: 20px 24px 0;
user-select: none;
}
.adspro-dialog-title {
font-size: 18px;
font-weight: 600;
color: #2D3748;
display: block;
}
/* Kolor tytułu per typ */
.adspro-dialog-type-red .adspro-dialog-title { color: #CC0000; }
.adspro-dialog-type-orange .adspro-dialog-title { color: #FF8C00; }
.adspro-dialog-type-green .adspro-dialog-title { color: #2F855A; }
.adspro-dialog-type-blue .adspro-dialog-title { color: #6690F4; }
/* --- Content --- */
.adspro-dialog-content-pane {
padding: 16px 24px;
transition: opacity 0.2s;
}
.adspro-dialog-content {
font-size: 14px;
line-height: 1.6;
color: #4E5E6A;
}
.adspro-dialog-content .form-group {
margin-bottom: 14px;
}
.adspro-dialog-content .form-control {
width: 100%;
}
/* --- Przyciski --- */
.adspro-dialog-buttons {
padding: 16px 24px 20px;
display: flex;
justify-content: flex-end;
gap: 10px;
border-top: 1px solid #F1F5F9;
transition: opacity 0.2s;
}
.adspro-dialog-btn {
padding: 9px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
border: none;
transition: all 0.2s;
font-family: inherit;
line-height: 1.4;
}
.adspro-dialog-btn:focus {
outline: none;
}
/* Klasy przycisków */
.adspro-dialog-btn.btn-blue {
background: #6690F4;
color: #fff;
}
.adspro-dialog-btn.btn-blue:hover {
background: #3164db;
}
.adspro-dialog-btn.btn-red {
background: #CC0000;
color: #fff;
}
.adspro-dialog-btn.btn-red:hover {
background: #b30000;
}
.adspro-dialog-btn.btn-green {
background: #57B951;
color: #fff;
}
.adspro-dialog-btn.btn-green:hover {
background: #4a9c3b;
}
.adspro-dialog-btn.btn-orange {
background: #FF8C00;
color: #fff;
}
.adspro-dialog-btn.btn-orange:hover {
background: #e07800;
}
.adspro-dialog-btn.btn-success {
background: #57B951;
color: #fff;
}
.adspro-dialog-btn.btn-success:hover {
background: #4a9c3b;
}
.adspro-dialog-btn.btn-danger {
background: #CC0000;
color: #fff;
}
.adspro-dialog-btn.btn-danger:hover {
background: #b30000;
}
.adspro-dialog-btn.btn-default {
background: #E2E8F0;
color: #4E5E6A;
}
.adspro-dialog-btn.btn-default:hover {
background: #CBD5E0;
}
/* --- Loading overlay --- */
.adspro-dialog-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 20;
border-radius: 12px;
}
.adspro-dialog-spinner {
width: 40px;
height: 40px;
border: 3px solid #E2E8F0;
border-top-color: #6690F4;
border-radius: 50%;
animation: adspro-dialog-spin 0.7s infinite linear;
}
/* --- Responsywność --- */
@media (max-width: 768px) {
.adspro-dialog-scrollpane {
padding: 20px 12px;
}
.adspro-dialog-box {
max-width: 100% !important;
}
.adspro-dialog-center[class*="col-"] {
max-width: 100% !important;
}
.adspro-dialog-buttons {
flex-direction: column;
}
.adspro-dialog-btn {
width: 100%;
text-align: center;
}
}

351
libraries/adspro-dialog.js Normal file
View File

@@ -0,0 +1,351 @@
/**
* AdsProDialog - własny system dialogów (zamiennik jquery-confirm)
* API kompatybilne z jquery-confirm: $.confirm(options), $.alert(options)
*/
(function( $ )
{
'use strict';
var dialogCounter = 0;
var activeDialogs = [];
function AdsProDialog( options )
{
this.id = ++dialogCounter;
this.options = $.extend( {}, AdsProDialog.defaults, options );
this.$el = null;
this.$box = null;
this.$content = null;
this._closed = false;
this._autoCloseTimer = null;
this._init();
}
AdsProDialog.defaults = {
title: '',
content: '',
type: '',
theme: '',
buttons: {},
closeIcon: false,
closeIconClass: 'fa-solid fa-xmark',
columnClass: '',
boxWidth: '',
useBootstrap: true,
draggable: false,
autoClose: '',
onContentReady: null,
onClose: null,
onOpen: null
};
AdsProDialog.prototype = {
_init: function()
{
this._buildDOM();
this._bindEvents();
this._appendToBody();
this._applyAutoClose();
this._triggerContentReady();
activeDialogs.push( this );
},
_buildDOM: function()
{
var o = this.options;
var typeClass = o.type ? ' adspro-dialog-type-' + o.type : '';
var sizeStyle = '';
var sizeClass = '';
if ( !o.useBootstrap && o.boxWidth )
{
sizeStyle = 'max-width:' + o.boxWidth + ';width:100%;';
}
else if ( o.columnClass )
{
sizeClass = ' ' + o.columnClass.replace( /col-md-offset-\d+/g, '' ).trim();
}
var html =
'<div class="adspro-dialog" data-dialog-id="' + this.id + '">' +
'<div class="adspro-dialog-bg"></div>' +
'<div class="adspro-dialog-scrollpane">' +
'<div class="adspro-dialog-center' + sizeClass + '">' +
'<div class="adspro-dialog-box jconfirm-box' + typeClass + '" style="' + sizeStyle + '">' +
this._buildCloseIcon( o ) +
this._buildHeader( o ) +
'<div class="adspro-dialog-content-pane">' +
'<div class="adspro-dialog-content">' + o.content + '</div>' +
'</div>' +
this._buildButtons( o ) +
'<div class="adspro-dialog-loading" style="display:none;">' +
'<div class="adspro-dialog-spinner"></div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
this.$el = $( html );
this.$box = this.$el.find( '.adspro-dialog-box' );
this.$content = this.$el.find( '.adspro-dialog-content' );
},
_buildCloseIcon: function( o )
{
if ( !o.closeIcon ) return '';
var iconClass = o.closeIconClass || 'fa-solid fa-xmark';
return '<div class="adspro-dialog-close-icon"><i class="' + iconClass + '"></i></div>';
},
_buildHeader: function( o )
{
if ( !o.title ) return '';
return '<div class="adspro-dialog-title-c">' +
'<span class="adspro-dialog-title">' + o.title + '</span>' +
'</div>';
},
_buildButtons: function( o )
{
if ( !o.buttons || $.isEmptyObject( o.buttons ) ) return '';
var html = '<div class="adspro-dialog-buttons">';
var self = this;
$.each( o.buttons, function( key, btnDef )
{
if ( typeof btnDef === 'function' )
{
btnDef = { action: btnDef };
o.buttons[ key ] = btnDef;
}
var text = btnDef.text || key.charAt( 0 ).toUpperCase() + key.slice( 1 );
var btnClass = btnDef.btnClass || 'btn-default';
var isEnter = ( btnDef.keys && btnDef.keys.indexOf( 'enter' ) !== -1 );
html += '<button type="button" class="adspro-dialog-btn btn ' + btnClass + '"' +
' data-btn-key="' + key + '"' +
( isEnter ? ' data-enter-key="true"' : '' ) +
'>' + text + '</button>';
});
html += '</div>';
return html;
},
_bindEvents: function()
{
var self = this;
var o = this.options;
// Backdrop click
this.$el.find( '.adspro-dialog-bg' ).on( 'click', function()
{
self.close();
});
// Close icon
this.$el.find( '.adspro-dialog-close-icon' ).on( 'click', function()
{
self.close();
});
// Buttons
this.$el.find( '.adspro-dialog-btn' ).each( function()
{
var $btn = $( this );
var key = $btn.data( 'btn-key' );
var btnDef = o.buttons[ key ];
// Referencje do buttonów (kompatybilność z $$formSubmit itp.)
self[ '$$' + key ] = $btn;
$btn.on( 'click', function()
{
if ( typeof btnDef === 'function' )
{
btnDef.call( self );
self.close();
return;
}
if ( btnDef && typeof btnDef.action === 'function' )
{
var result = btnDef.action.call( self );
if ( result !== false )
{
self.close();
}
}
else
{
self.close();
}
});
});
// Keyboard
$( document ).on( 'keydown.adspro-dialog-' + this.id, function( e )
{
if ( self._closed ) return;
if ( activeDialogs[ activeDialogs.length - 1 ] !== self ) return;
if ( e.key === 'Escape' )
{
e.preventDefault();
self.close();
}
if ( e.key === 'Enter' )
{
if ( $( e.target ).is( 'textarea, select' ) ) return;
var $enterBtn = self.$el.find( '.adspro-dialog-btn[data-enter-key="true"]' );
if ( $enterBtn.length )
{
e.preventDefault();
$enterBtn.trigger( 'click' );
}
}
});
// Draggable
if ( o.draggable && $.fn.draggable )
{
this.$box.draggable({
handle: '.adspro-dialog-title-c',
cursor: 'move'
});
this.$el.find( '.adspro-dialog-title-c' ).css( 'cursor', 'move' );
}
},
_appendToBody: function()
{
var baseZIndex = 99000 + ( this.id * 10 );
this.$el.css( 'z-index', baseZIndex );
$( 'body' ).append( this.$el );
var self = this;
requestAnimationFrame( function()
{
self.$el.addClass( 'adspro-dialog-open' );
});
if ( typeof this.options.onOpen === 'function' )
{
this.options.onOpen.call( this );
}
},
_applyAutoClose: function()
{
var ac = this.options.autoClose;
if ( !ac ) return;
var parts = ac.split( '|' );
if ( parts.length !== 2 ) return;
var ms = parseInt( parts[ 1 ], 10 );
var self = this;
this._autoCloseTimer = setTimeout( function()
{
if ( !self._closed ) self.close();
}, ms );
},
_triggerContentReady: function()
{
if ( typeof this.options.onContentReady === 'function' )
{
this.options.onContentReady.call( this );
}
},
// --- Metody publiczne ---
close: function()
{
if ( this._closed ) return;
this._closed = true;
if ( this._autoCloseTimer ) clearTimeout( this._autoCloseTimer );
$( document ).off( 'keydown.adspro-dialog-' + this.id );
var self = this;
this.$el.removeClass( 'adspro-dialog-open' );
this.$el.addClass( 'adspro-dialog-closing' );
setTimeout( function()
{
self.$el.remove();
var idx = activeDialogs.indexOf( self );
if ( idx > -1 ) activeDialogs.splice( idx, 1 );
if ( typeof self.options.onClose === 'function' )
{
self.options.onClose.call( self );
}
}, 250 );
},
showLoading: function( showContent )
{
this.$el.find( '.adspro-dialog-loading' ).show();
if ( !showContent )
{
this.$el.find( '.adspro-dialog-content-pane' ).css( 'opacity', '0.3' );
}
this.$el.find( '.adspro-dialog-buttons' ).css({
'pointer-events': 'none',
'opacity': '0.5'
});
},
hideLoading: function()
{
this.$el.find( '.adspro-dialog-loading' ).hide();
this.$el.find( '.adspro-dialog-content-pane' ).css( 'opacity', '' );
this.$el.find( '.adspro-dialog-buttons' ).css({
'pointer-events': '',
'opacity': ''
});
},
setContent: function( html )
{
this.$content.html( html );
},
setTitle: function( title )
{
this.$el.find( '.adspro-dialog-title' ).html( title );
}
};
// --- Rejestracja globalna ---
$.confirm = function( options )
{
return new AdsProDialog( options );
};
$.alert = function( options )
{
if ( typeof options === 'string' )
{
options = {
title: '',
content: options,
buttons: { ok: { text: 'OK', btnClass: 'btn-blue' } }
};
}
if ( !options.buttons )
{
options.buttons = { ok: { text: 'OK', btnClass: 'btn-blue' } };
}
return new AdsProDialog( options );
};
})( jQuery );

View File

@@ -1,6 +1,6 @@
-- Migracja: Tabela settings + kolumna google_ads_customer_id -- Migracja: Tabela settings + kolumny Google Ads w clients
-- Data: 2026-02-15 -- Data: 2026-02-15
-- Opis: Dodaje globalną tabelę ustawień key-value oraz kolumnę Google Ads Customer ID do tabeli clients -- Opis: Idempotentna migracja (bez bledow i bez duplikatow przy ponownym uruchomieniu)
-- 1. Tabela settings (globalne ustawienia aplikacji) -- 1. Tabela settings (globalne ustawienia aplikacji)
CREATE TABLE IF NOT EXISTS `settings` ( CREATE TABLE IF NOT EXISTS `settings` (
@@ -11,8 +11,34 @@ CREATE TABLE IF NOT EXISTS `settings` (
UNIQUE KEY `uk_setting_key` (`setting_key`) UNIQUE KEY `uk_setting_key` (`setting_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 2. Kolumna google_ads_customer_id w tabeli clients -- 2. Kolumna google_ads_customer_id w tabeli clients (tylko jesli nie istnieje)
ALTER TABLE `clients` ADD COLUMN `google_ads_customer_id` VARCHAR(20) NULL DEFAULT NULL AFTER `name`; SET @sql = IF(
EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'clients'
AND COLUMN_NAME = 'google_ads_customer_id'
),
'DO 1',
'ALTER TABLE `clients` ADD COLUMN `google_ads_customer_id` VARCHAR(20) NULL DEFAULT NULL AFTER `name`'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 3. Kolumna google_ads_start_date w tabeli clients (data od kiedy pobierać dane z Google Ads API) -- 3. Kolumna google_ads_start_date w tabeli clients (tylko jesli nie istnieje)
ALTER TABLE `clients` ADD COLUMN `google_ads_start_date` DATE NULL DEFAULT NULL AFTER `google_ads_customer_id`; SET @sql = IF(
EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'clients'
AND COLUMN_NAME = 'google_ads_start_date'
),
'DO 1',
'ALTER TABLE `clients` ADD COLUMN `google_ads_start_date` DATE NULL DEFAULT NULL AFTER `google_ads_customer_id`'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@@ -1 +1,17 @@
ALTER TABLE `products_data` ADD COLUMN `product_url` VARCHAR(500) NULL DEFAULT NULL; -- Migracja: products_data.product_url
-- Opis: Idempotentna migracja (bez bledu przy ponownym uruchomieniu)
SET @sql = IF(
EXISTS (
SELECT 1
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'products_data'
AND COLUMN_NAME = 'product_url'
),
'DO 1',
'ALTER TABLE `products_data` ADD COLUMN `product_url` VARCHAR(500) NULL DEFAULT NULL'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@@ -0,0 +1,77 @@
-- Migracja: grupy reklam + frazy wyszukiwane i wykluczajace dla kampanii
-- Opis: struktura pod import z Google Ads API (30 dni + all time)
CREATE TABLE IF NOT EXISTS `campaign_ad_groups` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`campaign_id` INT(11) NOT NULL,
`ad_group_id` BIGINT(20) NOT NULL,
`ad_group_name` VARCHAR(255) NOT NULL DEFAULT '',
`impressions_30` INT(11) NOT NULL DEFAULT 0,
`clicks_30` INT(11) NOT NULL DEFAULT 0,
`cost_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`conversions_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`roas_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`impressions_all_time` INT(11) NOT NULL DEFAULT 0,
`clicks_all_time` INT(11) NOT NULL DEFAULT 0,
`cost_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`conversions_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`roas_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`date_sync` DATE DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_campaign_ad_groups_campaign_ad_group` (`campaign_id`, `ad_group_id`),
KEY `idx_campaign_ad_groups_campaign_id` (`campaign_id`),
CONSTRAINT `FK_campaign_ad_groups_campaigns`
FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `campaign_search_terms` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`campaign_id` INT(11) NOT NULL,
`ad_group_id` INT(11) NOT NULL,
`search_term` VARCHAR(255) NOT NULL,
`impressions_30` INT(11) NOT NULL DEFAULT 0,
`clicks_30` INT(11) NOT NULL DEFAULT 0,
`cost_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`conversions_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`roas_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`impressions_all_time` INT(11) NOT NULL DEFAULT 0,
`clicks_all_time` INT(11) NOT NULL DEFAULT 0,
`cost_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`conversions_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`conversion_value_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`roas_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
`date_sync` DATE DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_campaign_search_terms` (`campaign_id`, `ad_group_id`, `search_term`),
KEY `idx_campaign_search_terms_campaign_id` (`campaign_id`),
KEY `idx_campaign_search_terms_ad_group_id` (`ad_group_id`),
CONSTRAINT `FK_campaign_search_terms_campaigns`
FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `FK_campaign_search_terms_ad_groups`
FOREIGN KEY (`ad_group_id`) REFERENCES `campaign_ad_groups` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `campaign_negative_keywords` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`campaign_id` INT(11) NOT NULL,
`ad_group_id` INT(11) DEFAULT NULL,
`scope` VARCHAR(20) NOT NULL DEFAULT 'campaign',
`keyword_text` VARCHAR(255) NOT NULL,
`match_type` VARCHAR(40) DEFAULT NULL,
`date_sync` DATE DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_campaign_negative_keywords_campaign_id` (`campaign_id`),
KEY `idx_campaign_negative_keywords_ad_group_id` (`ad_group_id`),
CONSTRAINT `FK_campaign_negative_keywords_campaigns`
FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `FK_campaign_negative_keywords_ad_groups`
FOREIGN KEY (`ad_group_id`) REFERENCES `campaign_ad_groups` (`id`)
ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -11,6 +11,23 @@
-- 1. KAMPANIE -- 1. KAMPANIE
-- ============================================================ -- ============================================================
-- Idempotencja: usuń poprzedni zestaw danych demo kampanii przed ponownym wstawieniem
DELETE ch
FROM campaigns_history ch
JOIN campaigns c ON c.id = ch.campaign_id
WHERE c.client_id = 2
AND c.campaign_id IN (
20845671001, 20845671002, 20845671003, 20845671004,
20845671005, 20845671006, 20845671007, 20845671008
);
DELETE FROM campaigns
WHERE client_id = 2
AND campaign_id IN (
20845671001, 20845671002, 20845671003, 20845671004,
20845671005, 20845671006, 20845671007, 20845671008
);
INSERT INTO `campaigns` (`client_id`, `campaign_id`, `campaign_name`) VALUES INSERT INTO `campaigns` (`client_id`, `campaign_id`, `campaign_name`) VALUES
(2, 20845671001, 'PMAX | Prezenty personalizowane'), (2, 20845671001, 'PMAX | Prezenty personalizowane'),
(2, 20845671002, 'PMAX | Bestsellery'), (2, 20845671002, 'PMAX | Bestsellery'),
@@ -152,6 +169,53 @@ DROP PROCEDURE IF EXISTS generate_campaign_history;
-- 3. PRODUKTY (25 produktów - realistyczne nazwy sklepu z prezentami) -- 3. PRODUKTY (25 produktów - realistyczne nazwy sklepu z prezentami)
-- ============================================================ -- ============================================================
-- Idempotencja: usuń poprzedni zestaw danych demo produktów przed ponownym wstawieniem
DELETE pt
FROM products_temp pt
JOIN products p ON p.id = pt.product_id
WHERE p.client_id = 2
AND p.offer_id IN (
'shopify_PL_8901001','shopify_PL_8901002','shopify_PL_8901003','shopify_PL_8901004','shopify_PL_8901005',
'shopify_PL_8901006','shopify_PL_8901007','shopify_PL_8901008','shopify_PL_8901009','shopify_PL_8901010',
'shopify_PL_8901011','shopify_PL_8901012','shopify_PL_8901013','shopify_PL_8901014','shopify_PL_8901015',
'shopify_PL_8901016','shopify_PL_8901017','shopify_PL_8901018','shopify_PL_8901019','shopify_PL_8901020',
'shopify_PL_8901021','shopify_PL_8901022','shopify_PL_8901023','shopify_PL_8901024','shopify_PL_8901025'
);
DELETE pd
FROM products_data pd
JOIN products p ON p.id = pd.product_id
WHERE p.client_id = 2
AND p.offer_id IN (
'shopify_PL_8901001','shopify_PL_8901002','shopify_PL_8901003','shopify_PL_8901004','shopify_PL_8901005',
'shopify_PL_8901006','shopify_PL_8901007','shopify_PL_8901008','shopify_PL_8901009','shopify_PL_8901010',
'shopify_PL_8901011','shopify_PL_8901012','shopify_PL_8901013','shopify_PL_8901014','shopify_PL_8901015',
'shopify_PL_8901016','shopify_PL_8901017','shopify_PL_8901018','shopify_PL_8901019','shopify_PL_8901020',
'shopify_PL_8901021','shopify_PL_8901022','shopify_PL_8901023','shopify_PL_8901024','shopify_PL_8901025'
);
DELETE ph
FROM products_history ph
JOIN products p ON p.id = ph.product_id
WHERE p.client_id = 2
AND p.offer_id IN (
'shopify_PL_8901001','shopify_PL_8901002','shopify_PL_8901003','shopify_PL_8901004','shopify_PL_8901005',
'shopify_PL_8901006','shopify_PL_8901007','shopify_PL_8901008','shopify_PL_8901009','shopify_PL_8901010',
'shopify_PL_8901011','shopify_PL_8901012','shopify_PL_8901013','shopify_PL_8901014','shopify_PL_8901015',
'shopify_PL_8901016','shopify_PL_8901017','shopify_PL_8901018','shopify_PL_8901019','shopify_PL_8901020',
'shopify_PL_8901021','shopify_PL_8901022','shopify_PL_8901023','shopify_PL_8901024','shopify_PL_8901025'
);
DELETE FROM products
WHERE client_id = 2
AND offer_id IN (
'shopify_PL_8901001','shopify_PL_8901002','shopify_PL_8901003','shopify_PL_8901004','shopify_PL_8901005',
'shopify_PL_8901006','shopify_PL_8901007','shopify_PL_8901008','shopify_PL_8901009','shopify_PL_8901010',
'shopify_PL_8901011','shopify_PL_8901012','shopify_PL_8901013','shopify_PL_8901014','shopify_PL_8901015',
'shopify_PL_8901016','shopify_PL_8901017','shopify_PL_8901018','shopify_PL_8901019','shopify_PL_8901020',
'shopify_PL_8901021','shopify_PL_8901022','shopify_PL_8901023','shopify_PL_8901024','shopify_PL_8901025'
);
INSERT INTO `products` (`client_id`, `offer_id`, `name`) VALUES INSERT INTO `products` (`client_id`, `offer_id`, `name`) VALUES
(2, 'shopify_PL_8901001', 'Kubek personalizowany ze zdjęciem - Biały 330ml'), (2, 'shopify_PL_8901001', 'Kubek personalizowany ze zdjęciem - Biały 330ml'),
(2, 'shopify_PL_8901002', 'Poduszka z własnym nadrukiem 40x40cm'), (2, 'shopify_PL_8901002', 'Poduszka z własnym nadrukiem 40x40cm'),
@@ -296,6 +360,12 @@ DROP PROCEDURE IF EXISTS generate_product_history;
-- 5. PRODUCTS_TEMP (zagregowane dane - jak po cron_products) -- 5. PRODUCTS_TEMP (zagregowane dane - jak po cron_products)
-- ============================================================ -- ============================================================
-- Idempotencja: wyczyść bieżące agregaty klienta przed ponownym przeliczeniem
DELETE pt
FROM products_temp pt
JOIN products p ON p.id = pt.product_id
WHERE p.client_id = 2;
INSERT INTO products_temp (product_id, name, impressions, impressions_30, clicks, clicks_30, ctr, cost, conversions, conversions_value, cpc, roas) INSERT INTO products_temp (product_id, name, impressions, impressions_30, clicks, clicks_30, ctr, cost, conversions, conversions_value, cpc, roas)
SELECT SELECT
p.id, p.id,

View File

@@ -0,0 +1,762 @@
<div class="campaigns-page campaign-terms-page">
<div class="campaigns-header">
<h2><i class="fa-solid fa-list-check"></i> Grupy i frazy</h2>
</div>
<div class="campaigns-filters">
<div class="filter-group">
<label for="terms_client_id"><i class="fa-solid fa-building"></i> Klient</label>
<select id="terms_client_id" name="terms_client_id" class="form-control">
<option value="">- wybierz klienta -</option>
<?php foreach ( $this -> clients as $client ): ?>
<option value="<?= $client['id']; ?>"><?= htmlspecialchars( $client['name'] ); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="filter-group">
<label for="terms_campaign_id"><i class="fa-solid fa-bullhorn"></i> Kampania</label>
<select id="terms_campaign_id" name="terms_campaign_id" class="form-control">
<option value="">- wybierz kampanie -</option>
</select>
</div>
<div class="filter-group">
<label for="terms_ad_group_id"><i class="fa-solid fa-layer-group"></i> Grupa reklam</label>
<select id="terms_ad_group_id" name="terms_ad_group_id" class="form-control">
<option value="">- wszystkie grupy -</option>
</select>
</div>
<div class="filter-group">
<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>
<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
</div>
<div class="campaigns-extra-table-wrap">
<table class="table table-sm campaigns-extra-table" id="terms_ad_groups_table">
<thead>
<tr>
<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>
</tr>
</thead>
<tbody>
<tr><td colspan="9" class="campaigns-empty-row">Wybierz klienta i kampanie.</td></tr>
</tbody>
</table>
</div>
</div>
<div class="campaigns-extra-card terms-card terms-search-card">
<div class="campaigns-extra-card-title">
<i class="fa-solid fa-magnifying-glass"></i> Frazy wyszukiwane (klikniecia >= 1)
</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>Akcja</th>
</tr>
</thead>
<tbody>
<tr><td colspan="11" class="campaigns-empty-row">Brak danych.</td></tr>
</tbody>
</table>
</div>
</div>
<div class="campaigns-extra-card terms-card terms-negative-card">
<div class="campaigns-extra-card-title">
<i class="fa-solid fa-ban"></i> Frazy wykluczajace
</div>
<div class="campaigns-extra-table-wrap">
<table class="table table-sm campaigns-extra-table" id="terms_negative_table">
<thead>
<tr>
<th>Poziom</th>
<th>Fraza</th>
<th>Match type</th>
</tr>
</thead>
<tbody>
<tr><td colspan="3" class="campaigns-empty-row">Brak danych.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<style>
.campaign-terms-wrap {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 20px;
}
.campaign-terms-page .campaigns-filters {
flex-wrap: wrap;
}
.campaign-terms-page .campaigns-filters .filter-group {
min-width: 220px;
}
.campaign-terms-page {
max-width: 100%;
overflow: hidden;
}
.campaigns-extra-card {
background: #FFFFFF;
border-radius: 10px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.campaigns-extra-card-title {
padding: 14px 16px;
border-bottom: 1px solid #E2E8F0;
font-size: 13px;
font-weight: 700;
color: #334155;
display: flex;
align-items: center;
gap: 8px;
}
.campaigns-extra-table-wrap {
overflow: auto;
}
.campaigns-extra-table {
margin: 0;
width: 100%;
table-layout: fixed;
}
.campaigns-extra-table thead th {
position: sticky;
top: 0;
z-index: 2;
background: #F8FAFC;
border-bottom: 1px solid #E2E8F0;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .4px;
color: #64748B;
padding: 10px 12px;
white-space: nowrap;
}
.campaigns-extra-table tbody td {
padding: 9px 12px;
border-bottom: 1px solid #F1F5F9;
font-size: 13px;
color: #334155;
vertical-align: middle;
white-space: nowrap;
}
.campaigns-extra-table td.num-cell {
text-align: right;
white-space: nowrap;
}
.campaigns-extra-table td.text-cell {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.campaigns-extra-table th.phrase-nowrap,
.campaigns-extra-table td.phrase-nowrap {
white-space: nowrap !important;
overflow: hidden;
text-overflow: ellipsis;
}
.campaigns-extra-table .terms-add-negative-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
border: 1px solid #E2E8F0;
background: #EEF2FF;
color: #3B82F6;
cursor: pointer;
transition: all 0.2s;
}
.campaigns-extra-table .terms-add-negative-btn:hover {
background: #3B82F6;
color: #FFFFFF;
border-color: #3B82F6;
}
.campaigns-extra-table tbody tr:hover {
background: #F8FAFC;
}
.campaigns-extra-table tbody tr.term-is-negative td {
color: #DC2626;
}
.campaigns-extra-table tbody tr.term-is-negative:hover {
background: #FEF2F2;
}
.campaigns-empty-row {
text-align: center;
color: #94A3B8 !important;
font-style: italic;
}
.campaign-terms-page .dt-layout-row:first-child {
display: none;
}
.campaign-terms-page .dt-layout-row {
padding: 10px 12px;
margin: 0 !important;
border-top: 1px solid #F1F5F9;
}
.campaign-terms-page .dt-info {
font-size: 12px;
color: #64748B;
}
.campaign-terms-page .dt-paging .pagination {
margin: 0;
padding: 0;
list-style: none !important;
display: flex;
align-items: center;
gap: 6px;
}
.campaign-terms-page .dt-paging .pagination .page-item {
list-style: none !important;
}
.campaign-terms-page .dt-paging .pagination .page-item .page-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 36px;
width: fit-content;
height: 32px;
padding: 0 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
border: 1px solid #E2E8F0;
background: #FFFFFF;
color: #4E5E6A;
text-decoration: none;
line-height: 1;
white-space: nowrap;
}
.campaign-terms-page .dt-paging .pagination .page-item.previous .page-link,
.campaign-terms-page .dt-paging .pagination .page-item.next .page-link {
min-width: 72px;
}
.campaign-terms-page .dt-paging .pagination .page-item .page-link:hover {
background: #EEF2FF;
color: #6690F4;
border-color: #6690F4;
}
.campaign-terms-page .dt-paging .pagination .page-item.active .page-link {
background: #6690F4;
color: #FFFFFF;
border-color: #6690F4;
}
.campaign-terms-page .dt-paging .pagination .page-item.disabled .page-link {
opacity: 0.35;
cursor: default;
pointer-events: none;
}
</style>
<script type="text/javascript">
var terms_ad_groups_table = null;
var terms_search_table = null;
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';
function terms_storage_set( key, value )
{
try
{
if ( value === null || value === undefined || value === '' )
localStorage.removeItem( key );
else
localStorage.setItem( key, String( value ) );
}
catch ( e ) {}
}
function terms_storage_get( key )
{
try
{
return localStorage.getItem( key ) || '';
}
catch ( e )
{
return '';
}
}
function format_num( value, digits )
{
var n = Number( value || 0 );
return n.toLocaleString( 'pl-PL', {
minimumFractionDigits: digits || 0,
maximumFractionDigits: digits || 0
} );
}
function destroy_table_if_exists( selector )
{
if ( $.fn.DataTable.isDataTable( selector ) )
{
$( selector ).DataTable().destroy();
$( selector + ' tbody' ).empty();
}
}
function build_ad_groups_table( rows )
{
destroy_table_if_exists( '#terms_ad_groups_table' );
terms_ad_groups_table = new DataTable( '#terms_ad_groups_table', {
data: rows || [],
processing: false,
serverSide: false,
autoWidth: false,
searching: false,
lengthChange: false,
pageLength: 15,
pagingType: 'simple_numbers',
order: [],
columns: [
{ data: 'ad_group_name', defaultContent: '' },
{ 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 ); } }
],
columnDefs: [
{ targets: 0, className: 'text-cell' },
{ targets: [1,5], className: 'num-cell', width: '80px' },
{ targets: [2,3,6,7], className: 'num-cell', width: '90px' },
{ targets: [4,8], className: 'num-cell', width: '85px' }
],
language: {
emptyTable: 'Brak danych do wyswietlenia',
info: 'Wpisy _START_ - _END_ z _TOTAL_',
paginate: { next: 'Dalej', previous: 'Wstecz' }
}
});
}
function build_search_terms_table( rows, negative_keywords )
{
destroy_table_if_exists( '#terms_search_table' );
var negative_set = {};
( negative_keywords || [] ).forEach( function( nk ) {
negative_set[ String( nk.keyword_text || '' ).toLowerCase().trim() ] = true;
});
terms_search_table = new DataTable( '#terms_search_table', {
data: rows || [],
processing: false,
serverSide: false,
autoWidth: false,
searching: true,
lengthChange: false,
pageLength: 15,
pagingType: 'simple_numbers',
order: [],
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: '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>';
} }
],
columnDefs: [
{ targets: 0, className: 'text-cell phrase-nowrap', width: '220px' },
{ targets: 1, className: 'text-cell' },
{ targets: [2,6], className: 'num-cell', width: '70px' },
{ targets: [3,4,7,8], className: 'num-cell', width: '85px' },
{ targets: [5,9], className: 'num-cell', width: '80px' },
{ targets: 10, className: 'dt-center', width: '50px' }
],
createdRow: function( row, data ) {
var term = String( data.search_term || '' ).toLowerCase().trim();
if ( negative_set[ term ] )
{
$( row ).addClass( 'term-is-negative' );
}
},
language: {
emptyTable: 'Brak danych do wyswietlenia',
info: 'Wpisy _START_ - _END_ z _TOTAL_',
paginate: { next: 'Dalej', previous: 'Wstecz' }
}
});
var q = $( '#phrase_search' ).val();
terms_search_table.search( q ).draw();
}
function build_negative_terms_table( rows )
{
destroy_table_if_exists( '#terms_negative_table' );
terms_negative_table = new DataTable( '#terms_negative_table', {
data: rows || [],
processing: false,
serverSide: false,
autoWidth: false,
searching: true,
lengthChange: false,
pageLength: 15,
pagingType: 'simple_numbers',
order: [],
columns: [
{ data: 'scope', render: function( data, type, row ) {
var scope = data === 'ad_group' ? 'Ad group' : 'Kampania';
if ( data === 'ad_group' && row.ad_group_name )
scope = 'Ad group: ' + row.ad_group_name;
return scope;
} },
{ data: 'keyword_text', defaultContent: '', width: '340px' },
{ data: 'match_type', defaultContent: '' }
],
columnDefs: [
{ targets: 0, className: 'text-cell' },
{ targets: 1, className: 'text-cell phrase-nowrap' },
{ targets: 2, width: '120px' }
],
language: {
emptyTable: 'Brak danych do wyswietlenia',
info: 'Wpisy _START_ - _END_ z _TOTAL_',
paginate: { next: 'Dalej', previous: 'Wstecz' }
}
});
var q = $( '#phrase_search' ).val();
terms_negative_table.search( q ).draw();
}
function reset_all_tables()
{
build_ad_groups_table( [] );
build_search_terms_table( [] );
build_negative_terms_table( [] );
}
function load_phrase_tables()
{
var campaign_id = $( '#terms_campaign_id' ).val();
var ad_group_id = $( '#terms_ad_group_id' ).val();
if ( !campaign_id )
{
reset_all_tables();
return;
}
$.ajax({
url: '/campaign_terms/get_campaign_phrase_details/',
type: 'POST',
data: {
campaign_id: campaign_id,
ad_group_id: ad_group_id
},
success: function( response )
{
var data = JSON.parse( response );
var negative_keywords = data.negative_keywords || [];
build_search_terms_table( data.search_terms || [], negative_keywords );
build_negative_terms_table( negative_keywords );
}
});
}
function load_ad_groups()
{
var campaign_id = $( '#terms_campaign_id' ).val();
var $ad_group_select = $( '#terms_ad_group_id' );
var saved_ad_group_id = terms_storage_get( TERMS_STORAGE_AD_GROUP );
$ad_group_select.empty();
$ad_group_select.append( '<option value="">- wszystkie grupy -</option>' );
if ( !campaign_id )
{
build_ad_groups_table( [] );
load_phrase_tables();
return;
}
$.ajax({
url: '/campaign_terms/get_campaign_ad_groups/',
type: 'POST',
data: { campaign_id: campaign_id },
success: function( response )
{
var data = JSON.parse( response );
var ad_groups = data.ad_groups || [];
build_ad_groups_table( ad_groups );
ad_groups.forEach( function( row ) {
$ad_group_select.append( '<option value="' + row.id + '">' + row.ad_group_name + '</option>' );
});
if ( saved_ad_group_id && $ad_group_select.find( 'option[value="' + saved_ad_group_id + '"]' ).length )
{
$ad_group_select.val( saved_ad_group_id );
}
load_phrase_tables();
}
});
}
function load_campaigns_for_client( restore_campaign_id )
{
var client_id = $( '#terms_client_id' ).val();
var $campaign_select = $( '#terms_campaign_id' );
$campaign_select.empty();
$campaign_select.append( '<option value="">- wybierz kampanie -</option>' );
if ( !client_id )
{
$( '#terms_ad_group_id' ).val( '' );
reset_all_tables();
return;
}
$.ajax({
url: '/campaign_terms/get_campaigns_list/client_id=' + client_id,
type: 'GET',
success: function( response )
{
var data = JSON.parse( response );
var campaigns = Object.entries( data.campaigns || {} );
campaigns.sort( function( a, b ) {
var nameA = String( ( a[1] && a[1].campaign_name ) ? a[1].campaign_name : '' ).toLowerCase();
var nameB = String( ( b[1] && b[1].campaign_name ) ? b[1].campaign_name : '' ).toLowerCase();
if ( nameA === '--- konto ---' ) return -1;
if ( nameB === '--- konto ---' ) return 1;
if ( nameA > nameB ) return 1;
if ( nameA < nameB ) return -1;
return 0;
});
campaigns.forEach( function( pair ) {
var value = pair[1];
$campaign_select.append( '<option value="' + value.id + '">' + value.campaign_name + '</option>' );
});
if ( restore_campaign_id && $campaign_select.find( 'option[value="' + restore_campaign_id + '"]' ).length )
{
$campaign_select.val( restore_campaign_id );
}
load_ad_groups();
}
});
}
$( function()
{
$( 'body' ).on( 'change', '#terms_client_id', function()
{
var client_id = $( this ).val();
terms_storage_set( TERMS_STORAGE_CLIENT, client_id );
terms_storage_set( TERMS_STORAGE_CAMPAIGN, '' );
terms_storage_set( TERMS_STORAGE_AD_GROUP, '' );
load_campaigns_for_client( '' );
});
$( 'body' ).on( 'change', '#terms_campaign_id', function()
{
var campaign_id = $( this ).val();
terms_storage_set( TERMS_STORAGE_CAMPAIGN, campaign_id );
terms_storage_set( TERMS_STORAGE_AD_GROUP, '' );
$( '#terms_ad_group_id' ).val( '' );
load_ad_groups();
});
$( 'body' ).on( 'change', '#terms_ad_group_id', function()
{
terms_storage_set( TERMS_STORAGE_AD_GROUP, $( this ).val() );
load_phrase_tables();
});
$( 'body' ).on( 'click', '.terms-add-negative-btn', function()
{
var search_term_id = $( this ).data( 'search-term-id' );
$.confirm({
title: 'Dodaj do wykluczajacych',
columnClass: 'col-md-4 col-md-offset-4',
content:
'<div class="form-group" style="margin-bottom:10px;">' +
'<label for="negative_scope" style="display:block;margin-bottom:6px;">Poziom wykluczenia</label>' +
'<select id="negative_scope" class="form-control">' +
'<option value="campaign" selected>Kampania (zalecane)</option>' +
'<option value="ad_group">Grupa reklam</option>' +
'</select>' +
'<small style="display:block;margin-top:6px;color:#64748B;">Kampania = widoczne w wykluczeniach kampanii. Grupa reklam = tylko w tej grupie.</small>' +
'</div>' +
'<div class="form-group" style="margin-bottom:0;">' +
'<label for="negative_match_type" style="display:block;margin-bottom:6px;">Typ dopasowania</label>' +
'<select id="negative_match_type" class="form-control">' +
'<option value="PHRASE" selected>Dopasowanie do wyrazenia</option>' +
'<option value="EXACT">Dopasowanie scisle</option>' +
'<option value="BROAD">Dopasowanie przyblizone</option>' +
'</select>' +
'</div>',
type: 'blue',
buttons: {
confirm: {
text: 'Zapisz',
btnClass: 'btn-blue',
action: function()
{
var match_type = this.$content.find( '#negative_match_type' ).val() || 'PHRASE';
var scope = this.$content.find( '#negative_scope' ).val() || 'campaign';
var modal = this;
modal.showLoading( true );
$.ajax({
url: '/campaign_terms/add_negative_keyword/',
type: 'POST',
data: {
search_term_id: search_term_id,
match_type: match_type,
scope: scope
},
success: function( response )
{
var data = JSON.parse( response );
modal.close();
if ( data.success )
{
var debugHtml = '';
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>';
}
var successDialog = $.alert({
title: 'Sukces',
columnClass: 'col-md-4 col-md-offset-4',
content: ( data.message || 'Fraza zostala dodana do wykluczajacych.' )
+ '<br><small style="display:block;margin-top:8px;color:#64748B;">Zmiana moze byc widoczna w panelu Google Ads po 1-3 minutach.</small>'
+ debugHtml,
type: 'green'
});
setTimeout( function() { successDialog.close(); }, 10000 );
load_phrase_tables();
}
else
{
var debugHtml = '';
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>';
}
$.alert({
title: 'Blad',
columnClass: 'col-md-4 col-md-offset-4',
content: ( data.message || 'Nie udalo sie dodac frazy.' ) + ( data.error ? '<br><small>' + data.error + '</small>' : '' ) + debugHtml,
type: 'red'
});
}
},
error: function()
{
modal.close();
$.alert({
title: 'Blad',
columnClass: 'col-md-4 col-md-offset-4',
content: 'Blad komunikacji z serwerem.',
type: 'red'
});
}
});
return false;
}
},
cancel: { text: 'Anuluj' }
}
});
});
$( 'body' ).on( 'input', '#phrase_search', function()
{
var q = $( this ).val();
if ( terms_search_table )
terms_search_table.search( q ).draw();
if ( terms_negative_table )
terms_negative_table.search( q ).draw();
});
reset_all_tables();
var saved_client_id = terms_storage_get( TERMS_STORAGE_CLIENT );
var saved_campaign_id = terms_storage_get( TERMS_STORAGE_CAMPAIGN );
if ( saved_client_id && $( '#terms_client_id option[value="' + saved_client_id + '"]' ).length )
{
$( '#terms_client_id' ).val( saved_client_id );
load_campaigns_for_client( saved_campaign_id );
}
});
</script>

View File

@@ -3,12 +3,11 @@
<h2><i class="fa-solid fa-chart-line"></i> Kampanie</h2> <h2><i class="fa-solid fa-chart-line"></i> Kampanie</h2>
</div> </div>
<!-- Filtry -->
<div class="campaigns-filters"> <div class="campaigns-filters">
<div class="filter-group"> <div class="filter-group">
<label for="client_id"><i class="fa-solid fa-building"></i> Klient</label> <label for="client_id"><i class="fa-solid fa-building"></i> Klient</label>
<select id="client_id" name="client_id" class="form-control"> <select id="client_id" name="client_id" class="form-control">
<option value=""> wybierz klienta </option> <option value="">- wybierz klienta -</option>
<?php foreach ( $this -> clients as $client ): ?> <?php foreach ( $this -> clients as $client ): ?>
<option value="<?= $client['id']; ?>"><?= htmlspecialchars( $client['name'] ); ?></option> <option value="<?= $client['id']; ?>"><?= htmlspecialchars( $client['name'] ); ?></option>
<?php endforeach; ?> <?php endforeach; ?>
@@ -18,21 +17,19 @@
<label for="campaign_id"><i class="fa-solid fa-bullhorn"></i> Kampania</label> <label for="campaign_id"><i class="fa-solid fa-bullhorn"></i> Kampania</label>
<div class="filter-with-action"> <div class="filter-with-action">
<select id="campaign_id" name="campaign_id" class="form-control"> <select id="campaign_id" name="campaign_id" class="form-control">
<option value=""> wybierz kampanię —</option> <option value="">- wybierz kampanie -</option>
</select> </select>
<button type="button" id="delete_campaign" class="btn-icon btn-icon-delete" title="Usuń kampanię"> <button type="button" id="delete_campaign" class="btn-icon btn-icon-delete" title="Usun kampanie">
<i class="fa-solid fa-trash"></i> <i class="fa-solid fa-trash"></i>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<!-- Wykres -->
<div class="campaigns-chart-wrap"> <div class="campaigns-chart-wrap">
<div id="container"></div> <div id="container"></div>
</div> </div>
<!-- Tabela historii -->
<div class="campaigns-table-wrap"> <div class="campaigns-table-wrap">
<table class="table" id="products"> <table class="table" id="products">
<thead> <thead>
@@ -40,22 +37,47 @@
<th>Data</th> <th>Data</th>
<th>ROAS (30 dni)</th> <th>ROAS (30 dni)</th>
<th>ROAS (all time)</th> <th>ROAS (all time)</th>
<th>Wartość konwersji (30 dni)</th> <th>Wartosc konwersji (30 dni)</th>
<th>Wydatki (30 dni)</th> <th>Wydatki (30 dni)</th>
<th>Komentarz</th> <th>Komentarz</th>
<th>Strategia ustalania stawek</th> <th>Strategia ustalania stawek</th>
<th>Budżet</th> <th>Budzet</th>
<th style="width: 60px; text-align: center;">Akcje</th> <th style="width: 60px; text-align: center;">Akcje</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody></tbody>
</tbody>
</table> </table>
</div> </div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
var client_id = ''; var STORAGE_CLIENT_KEY = 'campaigns.last_client_id';
var STORAGE_CAMPAIGN_KEY = 'campaigns.last_campaign_id';
var restore_campaign_after_client_load = '';
function storage_set( key, value )
{
try
{
if ( value === null || value === undefined || value === '' )
localStorage.removeItem( key );
else
localStorage.setItem( key, String( value ) );
}
catch ( e ) {}
}
function storage_get( key )
{
try
{
return localStorage.getItem( key ) || '';
}
catch ( e )
{
return '';
}
}
function reloadChart() function reloadChart()
{ {
@@ -68,8 +90,8 @@ function reloadChart()
data: { campaign_id: campaign_id }, data: { campaign_id: campaign_id },
success: function( response ) success: function( response )
{ {
const parsedData = JSON.parse( response ); var parsedData = JSON.parse( response );
let plotLines = []; var plotLines = [];
parsedData.comments.forEach( function( comment ) { parsedData.comments.forEach( function( comment ) {
plotLines.push({ plotLines.push({
@@ -140,11 +162,21 @@ function reloadChart()
$( function() $( function()
{ {
// Załaduj kampanie po wyborze klienta
$( 'body' ).on( 'change', '#client_id', function() $( 'body' ).on( 'change', '#client_id', function()
{ {
client_id = $( this ).val(); var client_id = $( this ).val();
storage_set( STORAGE_CLIENT_KEY, client_id );
var campaigns_select = $( '#campaign_id' ); var campaigns_select = $( '#campaign_id' );
var campaign_to_restore = restore_campaign_after_client_load;
if ( !campaign_to_restore )
storage_set( STORAGE_CAMPAIGN_KEY, '' );
campaigns_select.empty();
campaigns_select.append( '<option value="">- wybierz kampanie -</option>' );
if ( !client_id )
return;
$.ajax({ $.ajax({
url: '/campaigns/get_campaigns_list/client_id=' + client_id, url: '/campaigns/get_campaigns_list/client_id=' + client_id,
@@ -152,29 +184,33 @@ $( function()
success: function( response ) success: function( response )
{ {
var data = JSON.parse( response ); var data = JSON.parse( response );
campaigns_select.empty(); var campaigns = Object.entries( data.campaigns || {} );
campaigns_select.append( '<option value="">— wybierz kampanię —</option>' );
var campaigns = Object.entries( data.campaigns );
campaigns.sort( function( a, b ) { campaigns.sort( function( a, b ) {
if ( a[1] === "--- konto ---" ) return -1; var nameA = String( ( a[1] && a[1].campaign_name ) ? a[1].campaign_name : '' ).toLowerCase();
if ( b[1] === "--- konto ---" ) return 1; var nameB = String( ( b[1] && b[1].campaign_name ) ? b[1].campaign_name : '' ).toLowerCase();
return a[1] > b[1] ? 1 : ( a[1] < b[1] ? -1 : 0 ); if ( nameA === '--- konto ---' ) return -1;
if ( nameB === '--- konto ---' ) return 1;
if ( nameA > nameB ) return 1;
if ( nameA < nameB ) return -1;
return 0;
}); });
campaigns.forEach( function( [key, value] ) { campaigns.forEach( function( pair ) {
var value = pair[1];
campaigns_select.append( '<option value="' + value.id + '">' + value.campaign_name + '</option>' ); campaigns_select.append( '<option value="' + value.id + '">' + value.campaign_name + '</option>' );
}); });
<?php if ( $campaign_id ): ?> if ( campaign_to_restore && campaigns_select.find( 'option[value="' + campaign_to_restore + '"]' ).length )
campaigns_select.val( '<?= $campaign_id; ?>' ).trigger( 'change' ); {
<?php endif; ?> campaigns_select.val( campaign_to_restore ).trigger( 'change' );
}
restore_campaign_after_client_load = '';
} }
}); });
}); });
// Usuwanie kampanii
$( 'body' ).on( 'click', '#delete_campaign', function() $( 'body' ).on( 'click', '#delete_campaign', function()
{ {
var campaign_id = $( '#campaign_id' ).val(); var campaign_id = $( '#campaign_id' ).val();
@@ -184,19 +220,19 @@ $( function()
{ {
$.alert({ $.alert({
title: 'Uwaga', title: 'Uwaga',
content: 'Najpierw wybierz kampanię do usunięcia.', content: 'Najpierw wybierz kampanie do usuniecia.',
type: 'orange' type: 'orange'
}); });
return; return;
} }
$.confirm({ $.confirm({
title: 'Potwierdzenie usunięcia', title: 'Potwierdzenie usuniecia',
content: 'Czy na pewno chcesz usunąć kampanię <strong>' + campaign_name + '</strong>?<br><br>Ta operacja jest nieodwracalna i usunie również całą historię kampanii.', content: 'Czy na pewno chcesz usunac kampanie <strong>' + campaign_name + '</strong>?<br><br>Ta operacja jest nieodwracalna i usunie rowniez cala historie kampanii.',
type: 'red', type: 'red',
buttons: { buttons: {
confirm: { confirm: {
text: 'Usuń', text: 'Usun',
btnClass: 'btn-red', btnClass: 'btn-red',
keys: ['enter'], keys: ['enter'],
action: function() action: function()
@@ -209,9 +245,12 @@ $( function()
var data = JSON.parse( response ); var data = JSON.parse( response );
if ( data.success ) if ( data.success )
{ {
if ( storage_get( STORAGE_CAMPAIGN_KEY ) === String( campaign_id ) )
storage_set( STORAGE_CAMPAIGN_KEY, '' );
$.alert({ $.alert({
title: 'Sukces', title: 'Sukces',
content: 'Kampania została usunięta.', content: 'Kampania zostala usunieta.',
type: 'green', type: 'green',
autoClose: 'ok|2000' autoClose: 'ok|2000'
}); });
@@ -220,8 +259,8 @@ $( function()
else else
{ {
$.alert({ $.alert({
title: 'Błąd', title: 'Blad',
content: data.message || 'Nie udało się usunąć kampanii.', content: data.message || 'Nie udalo sie usunac kampanii.',
type: 'red' type: 'red'
}); });
} }
@@ -234,7 +273,6 @@ $( function()
}); });
}); });
// Usuwanie wpisu historii
$( 'body' ).on( 'click', '.delete-history-entry', function() $( 'body' ).on( 'click', '.delete-history-entry', function()
{ {
var btn = $( this ); var btn = $( this );
@@ -242,12 +280,12 @@ $( function()
var date = btn.data( 'date' ); var date = btn.data( 'date' );
$.confirm({ $.confirm({
title: 'Potwierdzenie usunięcia', title: 'Potwierdzenie usuniecia',
content: 'Czy na pewno chcesz usunąć wpis z dnia <strong>' + date + '</strong>?', content: 'Czy na pewno chcesz usunac wpis z dnia <strong>' + date + '</strong>?',
type: 'red', type: 'red',
buttons: { buttons: {
confirm: { confirm: {
text: 'Usuń', text: 'Usun',
btnClass: 'btn-red', btnClass: 'btn-red',
keys: ['enter'], keys: ['enter'],
action: function() action: function()
@@ -262,18 +300,19 @@ $( function()
{ {
$.alert({ $.alert({
title: 'Sukces', title: 'Sukces',
content: 'Wpis został usunięty.', content: 'Wpis zostal usuniety.',
type: 'green', type: 'green',
autoClose: 'ok|2000' autoClose: 'ok|2000'
}); });
$( '#products' ).DataTable().ajax.reload( null, false ); if ( $.fn.DataTable.isDataTable( '#products' ) )
$( '#products' ).DataTable().ajax.reload( null, false );
reloadChart(); reloadChart();
} }
else else
{ {
$.alert({ $.alert({
title: 'Błąd', title: 'Blad',
content: data.message || 'Nie udało się usunąć wpisu.', content: data.message || 'Nie udalo sie usunac wpisu.',
type: 'red' type: 'red'
}); });
} }
@@ -286,13 +325,19 @@ $( function()
}); });
}); });
// Załaduj dane po wyborze kampanii
$( 'body' ).on( 'change', '#campaign_id', function() $( 'body' ).on( 'change', '#campaign_id', function()
{ {
var campaign_id = $( this ).val(); var campaign_id = $( this ).val();
storage_set( STORAGE_CAMPAIGN_KEY, campaign_id );
table = $( '#products' ).DataTable(); if ( $.fn.DataTable.isDataTable( '#products' ) )
table.destroy(); {
$( '#products' ).DataTable().destroy();
$( '#products tbody' ).empty();
}
if ( !campaign_id )
return;
new DataTable( '#products', { new DataTable( '#products', {
ajax: { ajax: {
@@ -316,11 +361,11 @@ $( function()
{ width: '60px', name: 'actions', orderable: false, className: "dt-center" } { width: '60px', name: 'actions', orderable: false, className: "dt-center" }
], ],
language: { language: {
processing: 'Ładowanie...', processing: 'Ladowanie...',
emptyTable: 'Brak danych do wyświetlenia', emptyTable: 'Brak danych do wyswietlenia',
info: 'Wpisy _START_ - _END_ z _TOTAL_', info: 'Wpisy _START_ - _END_ z _TOTAL_',
infoEmpty: '', infoEmpty: '',
lengthMenu: 'Pokaż _MENU_ wpisów', lengthMenu: 'Pokaz _MENU_ wpisow',
paginate: { paginate: {
first: 'Pierwsza', first: 'Pierwsza',
last: 'Ostatnia', last: 'Ostatnia',
@@ -332,5 +377,14 @@ $( function()
reloadChart(); reloadChart();
}); });
var saved_client_id = storage_get( STORAGE_CLIENT_KEY );
var saved_campaign_id = storage_get( STORAGE_CAMPAIGN_KEY );
if ( saved_client_id && $( '#client_id option[value="' + saved_client_id + '"]' ).length )
{
restore_campaign_after_client_load = saved_campaign_id;
$( '#client_id' ).val( saved_client_id ).trigger( 'change' );
}
}); });
</script> </script>

View File

@@ -18,7 +18,7 @@
<script type="text/javascript" src="/libraries/datepicker/js/datepicker.min.js"></script> <script type="text/javascript" src="/libraries/datepicker/js/datepicker.min.js"></script>
<script type="text/javascript" src="/libraries/datepicker/js/i18n/datepicker.pl.js"></script> <script type="text/javascript" src="/libraries/datepicker/js/i18n/datepicker.pl.js"></script>
<script type="text/javascript" src="/libraries/daterange/daterangepicker.js"></script> <script type="text/javascript" src="/libraries/daterange/daterangepicker.js"></script>
<script type="text/javascript" src="/libraries/jquery-confirm/jquery-confirm.min.js"></script> <script type="text/javascript" src="/libraries/adspro-dialog.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="Stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.structure.min.css"> <link rel="Stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.structure.min.css">
<link rel="Stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.theme.min.css"> <link rel="Stylesheet" type="text/css" href="/libraries/framework/vendor/jquery/jquery_ui/jquery-ui.theme.min.css">
@@ -26,7 +26,7 @@
<link rel="Stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/daterange/daterangepicker.css"> <link rel="Stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/daterange/daterangepicker.css">
<link rel="stylesheet" type="text/css" href="/libraries/datepicker/css/datepicker.min.css"> <link rel="stylesheet" type="text/css" href="/libraries/datepicker/css/datepicker.min.css">
<link rel="Stylesheet" type="text/css" href="/libraries/daterange/daterangepicker.css"> <link rel="Stylesheet" type="text/css" href="/libraries/daterange/daterangepicker.css">
<link rel="stylesheet" type="text/css" href="/libraries/jquery-confirm/jquery-confirm.min.css"> <link rel="stylesheet" type="text/css" href="/libraries/adspro-dialog.css">
<link rel="stylesheet" type="text/css" href="/layout/style.css"> <link rel="stylesheet" type="text/css" href="/layout/style.css">
</head> </head>
<body class="logged"> <body class="logged">

View File

@@ -21,14 +21,14 @@
<script src="/libraries/framework/vendor/plugins/moment/pl.js"></script> <script src="/libraries/framework/vendor/plugins/moment/pl.js"></script>
<script src="/libraries/framework/vendor/plugins/datepicker/js/bootstrap-datetimepicker.js"></script> <script src="/libraries/framework/vendor/plugins/datepicker/js/bootstrap-datetimepicker.js"></script>
<script src="/libraries/framework/vendor/plugins/daterange/daterangepicker.js"></script> <script src="/libraries/framework/vendor/plugins/daterange/daterangepicker.js"></script>
<script src="/libraries/jquery-confirm/jquery-confirm.min.js"></script> <script src="/libraries/adspro-dialog.js">
<script src="/libraries/select2/js/select2.full.min.js"></script> <script src="/libraries/select2/js/select2.full.min.js"></script>
<script src="/libraries/functions.js"></script> <script src="/libraries/functions.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/2.1.7/css/dataTables.bootstrap5.min.css"> <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/2.1.7/css/dataTables.bootstrap5.min.css">
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/datepicker/css/bootstrap-datetimepicker.css"> <link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/datepicker/css/bootstrap-datetimepicker.css">
<link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/daterange/daterangepicker.css"> <link rel="stylesheet" type="text/css" href="/libraries/framework/vendor/plugins/daterange/daterangepicker.css">
<link rel="stylesheet" type="text/css" href="/libraries/jquery-confirm/jquery-confirm.min.css"> <link rel="stylesheet" type="text/css" href="/libraries/adspro-dialog.css">
<link rel="stylesheet" type="text/css" href="/libraries/select2/css/select2.min.css"> <link rel="stylesheet" type="text/css" href="/libraries/select2/css/select2.min.css">
<link rel="stylesheet" type="text/css" href="/libraries/select2/css/select2-bootstrap-5-theme.min.css"> <link rel="stylesheet" type="text/css" href="/libraries/select2/css/select2-bootstrap-5-theme.min.css">
<link rel="stylesheet" type="text/css" href="/layout/style.css"> <link rel="stylesheet" type="text/css" href="/layout/style.css">
@@ -58,6 +58,12 @@
<span>Kampanie</span> <span>Kampanie</span>
</a> </a>
</li> </li>
<li class="<?= $module === 'campaign_terms' ? 'active' : '' ?>">
<a href="/campaign_terms">
<i class="fa-solid fa-list-check"></i>
<span>Grupy i frazy</span>
</a>
</li>
<li class="<?= $module === 'products' ? 'active' : '' ?>"> <li class="<?= $module === 'products' ? 'active' : '' ?>">
<a href="/products"> <a href="/products">
<i class="fa-solid fa-box-open"></i> <i class="fa-solid fa-box-open"></i>
@@ -111,6 +117,7 @@
<?php <?php
$breadcrumbs = [ $breadcrumbs = [
'campaigns' => 'Kampanie', 'campaigns' => 'Kampanie',
'campaign_terms' => 'Grupy i frazy',
'products' => 'Produkty', 'products' => 'Produkty',
'clients' => 'Klienci', 'clients' => 'Klienci',
'allegro' => 'Allegro import', 'allegro' => 'Allegro import',