feat: Enhance user settings with cron URL plan display
- Added a new field to display the cron URL plan in user settings. - Updated JavaScript to handle the new plan data. refactor: Unify product model and migrate data - Migrated product data from `products_data` to `products` table. - Added new columns to `products` for better data organization. - Created `products_aggregate` table for storing aggregated product metrics. chore: Drop deprecated products_data table - Removed `products_data` table as data is now stored in `products`. feat: Add merchant URL flags to products - Introduced flags for tracking merchant URL status in `products` table. - Normalized product URLs to handle empty or invalid values. feat: Link campaign alerts to specific products - Added `product_id` column to `campaign_alerts` table for better tracking. - Created an index for efficient querying of alerts by product. chore: Add debug scripts for client data inspection - Created debug scripts to inspect client data from local and remote databases. - Included error handling and output formatting for better readability.
This commit is contained in:
4
.vscode/ftp-kr.sync.cache.json
vendored
4
.vscode/ftp-kr.sync.cache.json
vendored
@@ -53,8 +53,8 @@
|
|||||||
},
|
},
|
||||||
"class.Cron.php": {
|
"class.Cron.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 114130,
|
"size": 116612,
|
||||||
"lmtime": 1771496221250,
|
"lmtime": 1771511676177,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"class.Products.php": {
|
"class.Products.php": {
|
||||||
|
|||||||
@@ -448,7 +448,7 @@ class Api
|
|||||||
$history_30_rows++;
|
$history_30_rows++;
|
||||||
}
|
}
|
||||||
|
|
||||||
$temp_rows = \controls\Cron::rebuild_products_temp_for_client( $client_id );
|
$aggregate_rows = \controls\Cron::rebuild_products_temp_for_client( $client_id );
|
||||||
|
|
||||||
echo json_encode( [
|
echo json_encode( [
|
||||||
'status' => 'ok',
|
'status' => 'ok',
|
||||||
@@ -457,7 +457,7 @@ class Api
|
|||||||
'processed' => $processed,
|
'processed' => $processed,
|
||||||
'skipped' => $skipped,
|
'skipped' => $skipped,
|
||||||
'history_30_products' => $history_30_rows,
|
'history_30_products' => $history_30_rows,
|
||||||
'products_temp_rows' => $temp_rows
|
'products_aggregate_rows' => $aggregate_rows
|
||||||
] );
|
] );
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,33 @@ namespace controls;
|
|||||||
|
|
||||||
class CampaignAlerts
|
class CampaignAlerts
|
||||||
{
|
{
|
||||||
|
static private function redirect_to_main_view( $client_id = 0, $page = 1 )
|
||||||
|
{
|
||||||
|
$client_id = (int) $client_id;
|
||||||
|
$page = max( 1, (int) $page );
|
||||||
|
|
||||||
|
$query = [];
|
||||||
|
|
||||||
|
if ( $client_id > 0 )
|
||||||
|
{
|
||||||
|
$query[] = 'client_id=' . $client_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $page > 1 )
|
||||||
|
{
|
||||||
|
$query[] = 'page=' . $page;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = '/campaign_alerts';
|
||||||
|
if ( !empty( $query ) )
|
||||||
|
{
|
||||||
|
$url .= '?' . implode( '&', $query );
|
||||||
|
}
|
||||||
|
|
||||||
|
header( 'Location: ' . $url );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
static public function main_view()
|
static public function main_view()
|
||||||
{
|
{
|
||||||
$client_id = (int) \S::get( 'client_id' );
|
$client_id = (int) \S::get( 'client_id' );
|
||||||
@@ -31,4 +58,30 @@ class CampaignAlerts
|
|||||||
'total' => $total
|
'total' => $total
|
||||||
] );
|
] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static public function delete_selected()
|
||||||
|
{
|
||||||
|
$client_id = (int) \S::get( 'client_id' );
|
||||||
|
$page = max( 1, (int) \S::get( 'page' ) );
|
||||||
|
$alert_ids = \S::get( 'alert_ids' );
|
||||||
|
|
||||||
|
if ( !is_array( $alert_ids ) || empty( $alert_ids ) )
|
||||||
|
{
|
||||||
|
\S::alert( 'Nie zaznaczono alertow do usuniecia.' );
|
||||||
|
self::redirect_to_main_view( $client_id, $page );
|
||||||
|
}
|
||||||
|
|
||||||
|
$deleted = \factory\CampaignAlerts::delete_alerts( $alert_ids );
|
||||||
|
|
||||||
|
if ( $deleted > 0 )
|
||||||
|
{
|
||||||
|
\S::alert( $deleted === 1 ? 'Usunieto 1 alert.' : 'Usunieto ' . $deleted . ' alertow.' );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
\S::alert( 'Nie udalo sie usunac zaznaczonych alertow.' );
|
||||||
|
}
|
||||||
|
|
||||||
|
self::redirect_to_main_view( $client_id, $page );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,51 @@ namespace controls;
|
|||||||
|
|
||||||
class Clients
|
class Clients
|
||||||
{
|
{
|
||||||
|
static private function clients_has_deleted_column()
|
||||||
|
{
|
||||||
|
global $mdb;
|
||||||
|
|
||||||
|
static $has_deleted = null;
|
||||||
|
if ( $has_deleted !== null )
|
||||||
|
{
|
||||||
|
return (bool) $has_deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$stmt = $mdb -> pdo -> prepare( 'SHOW COLUMNS FROM clients LIKE :column_name' );
|
||||||
|
if ( $stmt )
|
||||||
|
{
|
||||||
|
$stmt -> bindValue( ':column_name', 'deleted', \PDO::PARAM_STR );
|
||||||
|
$stmt -> execute();
|
||||||
|
$has_deleted = $stmt -> fetch( \PDO::FETCH_ASSOC ) ? 1 : 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$has_deleted = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch ( \Throwable $e )
|
||||||
|
{
|
||||||
|
$has_deleted = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool) $has_deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
static private function sql_clients_not_deleted( $alias = '' )
|
||||||
|
{
|
||||||
|
$alias = trim( (string) $alias );
|
||||||
|
$prefix = $alias !== '' ? $alias . '.' : '';
|
||||||
|
|
||||||
|
if ( self::clients_has_deleted_column() )
|
||||||
|
{
|
||||||
|
return 'COALESCE(' . $prefix . 'deleted, 0) = 0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '1=1';
|
||||||
|
}
|
||||||
|
|
||||||
static public function main_view()
|
static public function main_view()
|
||||||
{
|
{
|
||||||
return \view\Clients::main_view(
|
return \view\Clients::main_view(
|
||||||
@@ -16,6 +61,8 @@ class Clients
|
|||||||
$name = trim( \S::get( 'name' ) );
|
$name = trim( \S::get( 'name' ) );
|
||||||
$google_ads_customer_id = trim( \S::get( 'google_ads_customer_id' ) );
|
$google_ads_customer_id = trim( \S::get( 'google_ads_customer_id' ) );
|
||||||
$google_merchant_account_id = trim( \S::get( 'google_merchant_account_id' ) );
|
$google_merchant_account_id = trim( \S::get( 'google_merchant_account_id' ) );
|
||||||
|
$active_raw = \S::get( 'active' );
|
||||||
|
$active = (string) $active_raw === '0' ? 0 : 1;
|
||||||
|
|
||||||
if ( !$name )
|
if ( !$name )
|
||||||
{
|
{
|
||||||
@@ -31,6 +78,7 @@ class Clients
|
|||||||
'google_ads_customer_id' => $google_ads_customer_id ?: null,
|
'google_ads_customer_id' => $google_ads_customer_id ?: null,
|
||||||
'google_merchant_account_id' => $google_merchant_account_id ?: null,
|
'google_merchant_account_id' => $google_merchant_account_id ?: null,
|
||||||
'google_ads_start_date' => $google_ads_start_date ?: null,
|
'google_ads_start_date' => $google_ads_start_date ?: null,
|
||||||
|
'active' => $active,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ( $id )
|
if ( $id )
|
||||||
@@ -48,6 +96,36 @@ class Clients
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static public function set_active()
|
||||||
|
{
|
||||||
|
$id = (int) \S::get( 'id' );
|
||||||
|
$active = (int) \S::get( 'active' ) === 1 ? 1 : 0;
|
||||||
|
|
||||||
|
if ( $id <= 0 )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'success' => false, 'message' => 'Brak ID klienta.' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = \factory\Clients::get( $id );
|
||||||
|
if ( !$client )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'success' => false, 'message' => 'Nie znaleziono klienta.' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( (int) ( $client['deleted'] ?? 0 ) === 1 )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'success' => false, 'message' => 'Nie mozna zmienic statusu usunietego klienta.' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
\factory\Clients::update( $id, [ 'active' => $active ] );
|
||||||
|
|
||||||
|
echo json_encode( [ 'success' => true, 'id' => $id, 'active' => $active ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
static public function delete()
|
static public function delete()
|
||||||
{
|
{
|
||||||
$id = \S::get( 'id' );
|
$id = \S::get( 'id' );
|
||||||
@@ -73,6 +151,7 @@ class Clients
|
|||||||
static public function sync_status()
|
static public function sync_status()
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
|
$clients_not_deleted_sql = self::sql_clients_not_deleted();
|
||||||
|
|
||||||
// Kampanie: 1 work unit per row (pending=0, done=1)
|
// Kampanie: 1 work unit per row (pending=0, done=1)
|
||||||
$campaigns_raw = $mdb->query(
|
$campaigns_raw = $mdb->query(
|
||||||
@@ -100,6 +179,32 @@ class Clients
|
|||||||
$data[ $row['client_id'] ]['products'] = [ (int) $row['done'], (int) $row['total'] ];
|
$data[ $row['client_id'] ]['products'] = [ (int) $row['done'], (int) $row['total'] ];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Walidacja Merchant (cron_campaigns_product_alerts_merchant) dziala na kursorze klienta.
|
||||||
|
// Pokazujemy postep per klient jako 0/1 albo 1/1 w aktualnym cyklu.
|
||||||
|
$merchant_cursor_client_id = (int) \services\GoogleAdsApi::get_setting( 'cron_campaigns_product_alerts_last_client_id' );
|
||||||
|
$merchant_clients_ids = $mdb -> query(
|
||||||
|
"SELECT id
|
||||||
|
FROM clients
|
||||||
|
WHERE " . $clients_not_deleted_sql . "
|
||||||
|
AND google_ads_customer_id IS NOT NULL
|
||||||
|
AND google_ads_customer_id <> ''
|
||||||
|
AND google_merchant_account_id IS NOT NULL
|
||||||
|
AND google_merchant_account_id <> ''
|
||||||
|
ORDER BY id ASC"
|
||||||
|
) -> fetchAll( \PDO::FETCH_COLUMN );
|
||||||
|
|
||||||
|
foreach ( (array) $merchant_clients_ids as $merchant_client_id )
|
||||||
|
{
|
||||||
|
$merchant_client_id = (int) $merchant_client_id;
|
||||||
|
if ( $merchant_client_id <= 0 )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$done = ( $merchant_cursor_client_id > 0 && $merchant_client_id <= $merchant_cursor_client_id ) ? 1 : 0;
|
||||||
|
$data[ $merchant_client_id ]['merchant'] = [ $done, 1 ];
|
||||||
|
}
|
||||||
|
|
||||||
echo json_encode( [ 'status' => 'ok', 'data' => $data ] );
|
echo json_encode( [ 'status' => 'ok', 'data' => $data ] );
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@@ -107,9 +212,10 @@ class Clients
|
|||||||
static public function force_sync()
|
static public function force_sync()
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
|
$clients_not_deleted_sql = self::sql_clients_not_deleted();
|
||||||
|
|
||||||
$id = (int) \S::get( 'id' );
|
$id = (int) \S::get( 'id' );
|
||||||
$pipeline = \S::get( 'pipeline' );
|
$pipeline = trim( (string) \S::get( 'pipeline' ) );
|
||||||
|
|
||||||
if ( !$id )
|
if ( !$id )
|
||||||
{
|
{
|
||||||
@@ -117,14 +223,62 @@ class Clients
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$where = [ 'client_id' => $id ];
|
$deleted_select = self::clients_has_deleted_column() ? 'COALESCE(deleted, 0) AS deleted' : '0 AS deleted';
|
||||||
|
$client = $mdb -> query(
|
||||||
|
"SELECT id, COALESCE(active, 0) AS active, " . $deleted_select . ", google_ads_customer_id, google_merchant_account_id
|
||||||
|
FROM clients
|
||||||
|
WHERE id = :id
|
||||||
|
LIMIT 1",
|
||||||
|
[ ':id' => $id ]
|
||||||
|
) -> fetch( \PDO::FETCH_ASSOC );
|
||||||
|
|
||||||
if ( in_array( $pipeline, [ 'campaigns', 'products' ] ) )
|
if ( !$client || (int) ( $client['deleted'] ?? 0 ) === 1 )
|
||||||
{
|
{
|
||||||
$where['pipeline'] = $pipeline;
|
echo json_encode( [ 'success' => false, 'message' => 'Nie znaleziono klienta.' ] );
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$mdb -> delete( 'cron_sync_status', $where );
|
if ( (int) ( $client['active'] ?? 0 ) !== 1 )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'success' => false, 'message' => 'Klient jest nieaktywny. Aktywuj klienta przed synchronizacja.' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( in_array( $pipeline, [ 'campaigns', 'products' ], true ) )
|
||||||
|
{
|
||||||
|
$where = [ 'client_id' => $id ];
|
||||||
|
$where['pipeline'] = $pipeline;
|
||||||
|
$mdb -> delete( 'cron_sync_status', $where );
|
||||||
|
}
|
||||||
|
else if ( $pipeline === 'campaigns_product_alerts_merchant' )
|
||||||
|
{
|
||||||
|
$has_ads_id = trim( (string) ( $client['google_ads_customer_id'] ?? '' ) ) !== '';
|
||||||
|
$has_merchant_id = trim( (string) ( $client['google_merchant_account_id'] ?? '' ) ) !== '';
|
||||||
|
if ( !$has_ads_id || !$has_merchant_id )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'success' => false, 'message' => 'Klient nie ma kompletnego Google Ads Customer ID i Merchant Account ID.' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$previous_eligible_id = (int) $mdb -> query(
|
||||||
|
"SELECT MAX(id)
|
||||||
|
FROM clients
|
||||||
|
WHERE " . $clients_not_deleted_sql . "
|
||||||
|
AND google_ads_customer_id IS NOT NULL
|
||||||
|
AND google_ads_customer_id <> ''
|
||||||
|
AND google_merchant_account_id IS NOT NULL
|
||||||
|
AND google_merchant_account_id <> ''
|
||||||
|
AND id < :client_id",
|
||||||
|
[ ':client_id' => $id ]
|
||||||
|
) -> fetchColumn();
|
||||||
|
|
||||||
|
\services\GoogleAdsApi::set_setting( 'cron_campaigns_product_alerts_last_client_id', (string) max( 0, $previous_eligible_id ) );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Domyslny reset (wszystkie pipeline oparte o cron_sync_status).
|
||||||
|
$mdb -> delete( 'cron_sync_status', [ 'client_id' => $id ] );
|
||||||
|
}
|
||||||
|
|
||||||
echo json_encode( [ 'success' => true, 'pipeline' => $pipeline ?: 'all' ] );
|
echo json_encode( [ 'success' => true, 'pipeline' => $pipeline ?: 'all' ] );
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -201,7 +201,15 @@ class Products
|
|||||||
static public function get_campaigns_list()
|
static public function get_campaigns_list()
|
||||||
{
|
{
|
||||||
$client_id = (int) \S::get( 'client_id' );
|
$client_id = (int) \S::get( 'client_id' );
|
||||||
echo json_encode( [ 'campaigns' => \factory\Campaigns::get_campaigns_list( $client_id, true ) ] );
|
$campaigns = \factory\Campaigns::get_campaigns_list( $client_id, true );
|
||||||
|
$allowed_channel_types = [ 'SHOPPING', 'PERFORMANCE_MAX' ];
|
||||||
|
|
||||||
|
$campaigns = array_values( array_filter( (array) $campaigns, function( $row ) use ( $allowed_channel_types ) {
|
||||||
|
$channel_type = strtoupper( trim( (string) ( $row['advertising_channel_type'] ?? '' ) ) );
|
||||||
|
return in_array( $channel_type, $allowed_channel_types, true );
|
||||||
|
} ) );
|
||||||
|
|
||||||
|
echo json_encode( [ 'campaigns' => $campaigns ] );
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +227,169 @@ class Products
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static public function get_scope_alerts()
|
||||||
|
{
|
||||||
|
$client_id = (int) \S::get( 'client_id' );
|
||||||
|
$campaign_id = (int) \S::get( 'campaign_id' );
|
||||||
|
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||||||
|
|
||||||
|
$alerts = \factory\Products::get_scope_alerts( $client_id, $campaign_id, $ad_group_id, 80 );
|
||||||
|
|
||||||
|
echo json_encode( [
|
||||||
|
'status' => 'ok',
|
||||||
|
'alerts' => $alerts,
|
||||||
|
'count' => count( $alerts )
|
||||||
|
] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function get_products_without_impressions_30()
|
||||||
|
{
|
||||||
|
$client_id = (int) \S::get( 'client_id' );
|
||||||
|
$campaign_id = (int) \S::get( 'campaign_id' );
|
||||||
|
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||||||
|
|
||||||
|
if ( $client_id <= 0 || $campaign_id <= 0 )
|
||||||
|
{
|
||||||
|
echo json_encode( [
|
||||||
|
'status' => 'ok',
|
||||||
|
'products' => [],
|
||||||
|
'count' => 0
|
||||||
|
] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = \factory\Products::get_products_without_impressions_30( $client_id, $campaign_id, $ad_group_id, 1000 );
|
||||||
|
$products = [];
|
||||||
|
|
||||||
|
foreach ( (array) $rows as $row )
|
||||||
|
{
|
||||||
|
$product_id = (int) ( $row['product_id'] ?? 0 );
|
||||||
|
if ( $product_id <= 0 )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$offer_id = trim( (string) ( $row['offer_id'] ?? '' ) );
|
||||||
|
$product_name = trim( (string) ( $row['title'] ?? '' ) );
|
||||||
|
if ( $product_name === '' )
|
||||||
|
{
|
||||||
|
$product_name = trim( (string) ( $row['name'] ?? '' ) );
|
||||||
|
}
|
||||||
|
if ( $product_name === '' )
|
||||||
|
{
|
||||||
|
$product_name = $offer_id !== '' ? $offer_id : ( 'Produkt #' . $product_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
$products[] = [
|
||||||
|
'product_id' => $product_id,
|
||||||
|
'offer_id' => $offer_id,
|
||||||
|
'name' => $product_name
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode( [
|
||||||
|
'status' => 'ok',
|
||||||
|
'products' => $products,
|
||||||
|
'count' => count( $products )
|
||||||
|
] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function delete_campaign_ad_group()
|
||||||
|
{
|
||||||
|
$campaign_id = (int) \S::get( 'campaign_id' );
|
||||||
|
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||||||
|
$delete_scope = trim( (string) \S::get( 'delete_scope' ) );
|
||||||
|
|
||||||
|
if ( $ad_group_id <= 0 )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'status' => 'error', 'message' => 'Nie wybrano grupy reklam do usuniecia.' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !in_array( $delete_scope, [ 'local', 'google' ], true ) )
|
||||||
|
{
|
||||||
|
$delete_scope = 'local';
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = \factory\Products::get_ad_group_delete_context( $ad_group_id );
|
||||||
|
if ( !$context )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'status' => 'ok', 'message' => 'Grupa reklam byla juz usunieta lokalnie.' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$local_campaign_id = (int) ( $context['local_campaign_id'] ?? 0 );
|
||||||
|
if ( $campaign_id > 0 && $campaign_id !== $local_campaign_id )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'status' => 'error', 'message' => 'Wybrana grupa reklam nie nalezy do wskazanej kampanii.' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$channel_type = strtoupper( trim( (string) ( $context['advertising_channel_type'] ?? '' ) ) );
|
||||||
|
if ( $channel_type !== 'SHOPPING' )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'status' => 'error', 'message' => 'Usuwanie grup reklam jest dostepne tylko dla kampanii produktowych (Shopping).' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $delete_scope === 'google' )
|
||||||
|
{
|
||||||
|
$customer_id = preg_replace( '/\D+/', '', (string) ( $context['google_ads_customer_id'] ?? '' ) );
|
||||||
|
$external_ad_group_id = (int) ( $context['external_ad_group_id'] ?? 0 );
|
||||||
|
|
||||||
|
if ( $customer_id === '' || $external_ad_group_id <= 0 )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'status' => 'error', 'message' => 'Brak danych Google Ads (customer_id lub ad_group_id).' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$google_ads_api = new \services\GoogleAdsApi();
|
||||||
|
if ( !$google_ads_api -> is_configured() )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'status' => 'error', 'message' => 'Google Ads API nie jest skonfigurowane.' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$google_result = $google_ads_api -> remove_ad_group( $customer_id, $external_ad_group_id );
|
||||||
|
if ( empty( $google_result['success'] ) )
|
||||||
|
{
|
||||||
|
$error_message = trim( (string) ( $google_result['error'] ?? \services\GoogleAdsApi::get_setting( 'google_ads_last_error' ) ) );
|
||||||
|
if ( $error_message === '' )
|
||||||
|
{
|
||||||
|
$error_message = 'Nie udalo sie usunac grupy reklam w Google Ads.';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode( [ 'status' => 'error', 'message' => $error_message ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !\factory\Products::delete_ad_group_local( $ad_group_id ) )
|
||||||
|
{
|
||||||
|
$still_exists_local = \factory\Products::get_ad_group_scope_context( $ad_group_id );
|
||||||
|
if ( !$still_exists_local )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'status' => 'ok', 'message' => 'Grupa reklam zostala usunieta lokalnie.' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode( [ 'status' => 'error', 'message' => 'Nie udalo sie usunac grupy reklam lokalnie.' ] );
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $delete_scope === 'google' )
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'status' => 'ok', 'message' => 'Usunieto grupe reklam lokalnie oraz w Google Ads.' ] );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
echo json_encode( [ 'status' => 'ok', 'message' => 'Usunieto grupe reklam lokalnie.' ] );
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
static public function assign_product_scope()
|
static public function assign_product_scope()
|
||||||
{
|
{
|
||||||
$product_id = (int) \S::get( 'product_id' );
|
$product_id = (int) \S::get( 'product_id' );
|
||||||
|
|||||||
@@ -154,7 +154,19 @@ class Users
|
|||||||
global $mdb;
|
global $mdb;
|
||||||
|
|
||||||
$base_url = self::get_base_url();
|
$base_url = self::get_base_url();
|
||||||
$clients_total = (int) $mdb -> query( "SELECT COUNT(*) FROM clients WHERE deleted = 0 AND google_ads_customer_id IS NOT NULL AND google_ads_customer_id <> ''" ) -> fetchColumn();
|
$clients_total = (int) $mdb -> query(
|
||||||
|
"SELECT COUNT(*)
|
||||||
|
FROM clients
|
||||||
|
WHERE COALESCE( active, 0 ) = 1
|
||||||
|
AND TRIM( COALESCE( google_ads_customer_id, '' ) ) <> ''"
|
||||||
|
) -> fetchColumn();
|
||||||
|
$merchant_clients_total = (int) $mdb -> query(
|
||||||
|
"SELECT COUNT(*)
|
||||||
|
FROM clients
|
||||||
|
WHERE COALESCE( active, 0 ) = 1
|
||||||
|
AND TRIM( COALESCE( google_ads_customer_id, '' ) ) <> ''
|
||||||
|
AND TRIM( COALESCE( google_merchant_account_id, '' ) ) <> ''"
|
||||||
|
) -> fetchColumn();
|
||||||
|
|
||||||
// --- Kampanie ---
|
// --- Kampanie ---
|
||||||
$campaign_stats = $mdb -> query(
|
$campaign_stats = $mdb -> query(
|
||||||
@@ -203,7 +215,7 @@ class Users
|
|||||||
if ( $products_active_date )
|
if ( $products_active_date )
|
||||||
{
|
{
|
||||||
$current_phase = $mdb -> query(
|
$current_phase = $mdb -> query(
|
||||||
"SELECT phase FROM cron_sync_status cs INNER JOIN clients c ON cs.client_id = c.id AND c.deleted = 0 WHERE cs.pipeline = 'products' AND cs.sync_date = :sync_date AND cs.phase != 'done' ORDER BY FIELD(cs.phase, 'pending', 'fetch', 'aggregate_30') ASC LIMIT 1",
|
"SELECT phase FROM cron_sync_status cs INNER JOIN clients c ON cs.client_id = c.id AND COALESCE(c.active, 0) = 1 WHERE cs.pipeline = 'products' AND cs.sync_date = :sync_date AND cs.phase != 'done' ORDER BY FIELD(cs.phase, 'pending', 'fetch', 'aggregate_30') ASC LIMIT 1",
|
||||||
[ ':sync_date' => $products_active_date ]
|
[ ':sync_date' => $products_active_date ]
|
||||||
) -> fetchColumn();
|
) -> fetchColumn();
|
||||||
|
|
||||||
@@ -222,16 +234,45 @@ class Users
|
|||||||
$products_meta .= ', ' . $products_eta_meta;
|
$products_meta .= ', ' . $products_eta_meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Walidacja Merchant dla alertow kampanii ---
|
||||||
|
$merchant_cursor_client_id = (int) \services\GoogleAdsApi::get_setting( 'cron_campaigns_product_alerts_last_client_id' );
|
||||||
|
$merchant_processed = 0;
|
||||||
|
if ( $merchant_cursor_client_id > 0 && $merchant_clients_total > 0 )
|
||||||
|
{
|
||||||
|
$merchant_processed = (int) $mdb -> query(
|
||||||
|
"SELECT COUNT(*)
|
||||||
|
FROM clients
|
||||||
|
WHERE COALESCE( active, 0 ) = 1
|
||||||
|
AND TRIM( COALESCE( google_ads_customer_id, '' ) ) <> ''
|
||||||
|
AND TRIM( COALESCE( google_merchant_account_id, '' ) ) <> ''
|
||||||
|
AND id <= :cursor_client_id",
|
||||||
|
[ ':cursor_client_id' => $merchant_cursor_client_id ]
|
||||||
|
) -> fetchColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
$merchant_processed = min( $merchant_clients_total, max( 0, $merchant_processed ) );
|
||||||
|
$merchant_remaining = max( 0, $merchant_clients_total - $merchant_processed );
|
||||||
|
$merchant_meta = 'Kursor klienta: ' . ( $merchant_cursor_client_id > 0 ? '#' . $merchant_cursor_client_id : '-' ) . ', klienci z Merchant ID: ' . $merchant_clients_total;
|
||||||
|
$merchant_eta_meta = self::build_eta_meta( 'cron_campaigns_product_alerts_merchant', $merchant_remaining );
|
||||||
|
if ( $merchant_eta_meta !== '' )
|
||||||
|
{
|
||||||
|
$merchant_meta .= ', ' . $merchant_eta_meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cron_schedule = [];
|
||||||
|
|
||||||
// --- Endpointy CRON ---
|
// --- Endpointy CRON ---
|
||||||
$cron_endpoints = [
|
$cron_endpoints = [
|
||||||
[ 'name' => 'Legacy CRON', 'path' => '/cron.php', 'action' => 'cron_legacy' ],
|
[ 'name' => 'Legacy CRON', 'path' => '/cron.php', 'action' => 'cron_legacy', 'plan' => '' ],
|
||||||
[ 'name' => 'Cron kampanii', 'path' => '/cron/cron_campaigns', 'action' => 'cron_campaigns' ],
|
[ 'name' => 'Cron zbiorczy (K->P->GMC)', 'path' => '/cron/cron_clients_bundle', 'action' => 'cron_clients_bundle', 'plan' => 'Co 1 min: klient -> kampanie (-7..-1) -> produkty (-7..-1) -> GMC' ],
|
||||||
[ 'name' => 'Cron produktów', 'path' => '/cron/cron_products', 'action' => 'cron_products' ],
|
[ 'name' => 'Cron kampanii', 'path' => '/cron/cron_campaigns', 'action' => 'cron_campaigns', 'plan' => 'Krok 1/2, co 15 min' ],
|
||||||
[ 'name' => 'Cron URL produktów (Merchant)', 'path' => '/cron/cron_products_urls', 'action' => 'cron_products_urls' ],
|
[ 'name' => 'Cron alertow kampanii (Merchant)', 'path' => '/cron/cron_campaigns_product_alerts_merchant', 'action' => 'cron_campaigns_product_alerts_merchant', 'plan' => 'Krok 2/2, 2-5 min po Cron kampanii, co 15 min' ],
|
||||||
[ 'name' => 'Cron fraz', 'path' => '/cron/cron_phrases', 'action' => 'cron_phrases' ],
|
[ 'name' => 'Cron produktów', 'path' => '/cron/cron_products', 'action' => 'cron_products', 'plan' => '' ],
|
||||||
[ 'name' => 'Historia 30 dni produktów', 'path' => '/cron/cron_products_history_30', 'action' => 'cron_products_history_30' ],
|
[ 'name' => 'Cron URL produktów (Merchant)', 'path' => '/cron/cron_products_urls', 'action' => 'cron_products_urls', 'plan' => '' ],
|
||||||
[ 'name' => 'Historia 30 dni fraz', 'path' => '/cron/cron_phrases_history_30', 'action' => 'cron_phrases_history_30' ],
|
[ 'name' => 'Cron fraz', 'path' => '/cron/cron_phrases', 'action' => 'cron_phrases', 'plan' => '' ],
|
||||||
[ 'name' => 'Eksport XML', 'path' => '/cron/cron_xml', 'action' => 'cron_xml' ],
|
[ 'name' => 'Historia 30 dni produktów', 'path' => '/cron/cron_products_history_30', 'action' => 'cron_products_history_30', 'plan' => '' ],
|
||||||
|
[ 'name' => 'Historia 30 dni fraz', 'path' => '/cron/cron_phrases_history_30', 'action' => 'cron_phrases_history_30', 'plan' => '' ],
|
||||||
|
[ 'name' => 'Eksport XML', 'path' => '/cron/cron_xml', 'action' => 'cron_xml', 'plan' => '' ],
|
||||||
];
|
];
|
||||||
|
|
||||||
$urls = [];
|
$urls = [];
|
||||||
@@ -242,6 +283,7 @@ class Users
|
|||||||
'name' => $endpoint['name'],
|
'name' => $endpoint['name'],
|
||||||
'url' => $base_url . $endpoint['path'],
|
'url' => $base_url . $endpoint['path'],
|
||||||
'last_invoked_at' => self::format_datetime( \services\GoogleAdsApi::get_setting( $last_key ) ),
|
'last_invoked_at' => self::format_datetime( \services\GoogleAdsApi::get_setting( $last_key ) ),
|
||||||
|
'plan' => (string) ( $endpoint['plan'] ?? '' ),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +305,15 @@ class Users
|
|||||||
'percent' => self::progress_percent( $products_processed, $products_total ),
|
'percent' => self::progress_percent( $products_processed, $products_total ),
|
||||||
'meta' => $products_meta
|
'meta' => $products_meta
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Walidacja Merchant',
|
||||||
|
'processed' => $merchant_processed,
|
||||||
|
'total' => $merchant_clients_total,
|
||||||
|
'percent' => self::progress_percent( $merchant_processed, $merchant_clients_total ),
|
||||||
|
'meta' => $merchant_meta
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
'schedule' => $cron_schedule,
|
||||||
'urls' => $urls
|
'urls' => $urls
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class CampaignAlerts
|
|||||||
ca.campaign_external_id,
|
ca.campaign_external_id,
|
||||||
ca.ad_group_id,
|
ca.ad_group_id,
|
||||||
ca.ad_group_external_id,
|
ca.ad_group_external_id,
|
||||||
|
ca.product_id,
|
||||||
ca.alert_type,
|
ca.alert_type,
|
||||||
ca.message,
|
ca.message,
|
||||||
ca.meta_json,
|
ca.meta_json,
|
||||||
@@ -109,4 +110,29 @@ class CampaignAlerts
|
|||||||
'date_detected[<]' => date( 'Y-m-d', strtotime( '-' . (int) $days . ' days' ) )
|
'date_detected[<]' => date( 'Y-m-d', strtotime( '-' . (int) $days . ' days' ) )
|
||||||
] );
|
] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static public function delete_alerts( array $ids )
|
||||||
|
{
|
||||||
|
global $mdb;
|
||||||
|
|
||||||
|
$ids = array_map( 'intval', $ids );
|
||||||
|
$ids = array_filter( $ids, function( $id ) { return $id > 0; } );
|
||||||
|
|
||||||
|
if ( empty( $ids ) )
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = (int) $mdb -> count( 'campaign_alerts', [ 'id' => $ids ] );
|
||||||
|
if ( $before <= 0 )
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mdb -> delete( 'campaign_alerts', [ 'id' => $ids ] );
|
||||||
|
|
||||||
|
$after = (int) $mdb -> count( 'campaign_alerts', [ 'id' => $ids ] );
|
||||||
|
|
||||||
|
return max( 0, $before - $after );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,18 @@
|
|||||||
namespace factory;
|
namespace factory;
|
||||||
class Products
|
class Products
|
||||||
{
|
{
|
||||||
|
static private function is_product_core_field( $field )
|
||||||
|
{
|
||||||
|
return in_array( (string) $field, [
|
||||||
|
'custom_label_4',
|
||||||
|
'custom_label_3',
|
||||||
|
'title',
|
||||||
|
'description',
|
||||||
|
'google_product_category',
|
||||||
|
'product_url'
|
||||||
|
], true );
|
||||||
|
}
|
||||||
|
|
||||||
static public function delete_product( $product_id ) {
|
static public function delete_product( $product_id ) {
|
||||||
global $mdb;
|
global $mdb;
|
||||||
$mdb -> delete( 'products', [ 'id' => $product_id ] );
|
$mdb -> delete( 'products', [ 'id' => $product_id ] );
|
||||||
@@ -31,12 +43,162 @@ class Products
|
|||||||
return $mdb -> delete( 'products_comments', [ 'id' => $comment_id ] );
|
return $mdb -> delete( 'products_comments', [ 'id' => $comment_id ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static public function get_ad_group_delete_context( $ad_group_id )
|
||||||
|
{
|
||||||
|
global $mdb;
|
||||||
|
|
||||||
|
return $mdb -> query(
|
||||||
|
'SELECT
|
||||||
|
ag.id AS local_ad_group_id,
|
||||||
|
ag.campaign_id AS local_campaign_id,
|
||||||
|
ag.ad_group_id AS external_ad_group_id,
|
||||||
|
ag.ad_group_name,
|
||||||
|
c.client_id,
|
||||||
|
c.campaign_id AS external_campaign_id,
|
||||||
|
c.campaign_name,
|
||||||
|
c.advertising_channel_type,
|
||||||
|
cl.google_ads_customer_id
|
||||||
|
FROM campaign_ad_groups AS ag
|
||||||
|
INNER JOIN campaigns AS c ON c.id = ag.campaign_id
|
||||||
|
INNER JOIN clients AS cl ON cl.id = c.client_id
|
||||||
|
WHERE ag.id = :ad_group_id
|
||||||
|
LIMIT 1',
|
||||||
|
[ ':ad_group_id' => (int) $ad_group_id ]
|
||||||
|
) -> fetch( \PDO::FETCH_ASSOC );
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function delete_ad_group_local( $ad_group_id )
|
||||||
|
{
|
||||||
|
global $mdb;
|
||||||
|
|
||||||
|
$ad_group_id = (int) $ad_group_id;
|
||||||
|
if ( $ad_group_id <= 0 )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mdb -> delete( 'campaign_ad_groups', [ 'id' => $ad_group_id ] );
|
||||||
|
|
||||||
|
if ( (int) $mdb -> rowCount() > 0 )
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traktuj jako sukces, jeżeli wpis i tak już nie istnieje.
|
||||||
|
$exists = (int) $mdb -> count( 'campaign_ad_groups', [ 'id' => $ad_group_id ] );
|
||||||
|
return $exists === 0;
|
||||||
|
}
|
||||||
|
|
||||||
static public function get_product_comment_by_date( $product_id, $date )
|
static public function get_product_comment_by_date( $product_id, $date )
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
return $mdb -> get( 'products_comments', [ 'id', 'comment' ], [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] );
|
return $mdb -> get( 'products_comments', [ 'id', 'comment' ], [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static public function get_scope_alerts( $client_id, $campaign_id, $ad_group_id, $limit = 50 )
|
||||||
|
{
|
||||||
|
global $mdb;
|
||||||
|
|
||||||
|
$client_id = (int) $client_id;
|
||||||
|
$campaign_id = (int) $campaign_id;
|
||||||
|
$ad_group_id = (int) $ad_group_id;
|
||||||
|
$limit = max( 1, (int) $limit );
|
||||||
|
|
||||||
|
if ( $client_id <= 0 || $campaign_id <= 0 )
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$where = [
|
||||||
|
'client_id' => $client_id,
|
||||||
|
'campaign_id' => $campaign_id
|
||||||
|
];
|
||||||
|
|
||||||
|
if ( $ad_group_id > 0 )
|
||||||
|
{
|
||||||
|
$where['ad_group_id'] = $ad_group_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $mdb -> select( 'campaign_alerts', [
|
||||||
|
'id',
|
||||||
|
'alert_type',
|
||||||
|
'message',
|
||||||
|
'date_detected',
|
||||||
|
'date_add'
|
||||||
|
], [
|
||||||
|
'AND' => $where,
|
||||||
|
'ORDER' => [
|
||||||
|
'date_detected' => 'DESC',
|
||||||
|
'id' => 'DESC'
|
||||||
|
],
|
||||||
|
'LIMIT' => $limit
|
||||||
|
] );
|
||||||
|
|
||||||
|
return is_array( $rows ) ? $rows : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
static public function get_products_without_impressions_30( $client_id, $campaign_id, $ad_group_id = 0, $limit = 500 )
|
||||||
|
{
|
||||||
|
global $mdb;
|
||||||
|
|
||||||
|
$client_id = (int) $client_id;
|
||||||
|
$campaign_id = (int) $campaign_id;
|
||||||
|
$ad_group_id = (int) $ad_group_id;
|
||||||
|
$limit = max( 1, (int) $limit );
|
||||||
|
$limit = min( 2000, $limit );
|
||||||
|
|
||||||
|
if ( $client_id <= 0 || $campaign_id <= 0 )
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
':client_id' => $client_id,
|
||||||
|
':campaign_id' => $campaign_id
|
||||||
|
];
|
||||||
|
|
||||||
|
$sql = 'SELECT
|
||||||
|
p.id AS product_id,
|
||||||
|
p.offer_id,
|
||||||
|
p.name,
|
||||||
|
p.title,
|
||||||
|
SUM( pa.impressions_30 ) AS impressions_30
|
||||||
|
FROM products_aggregate AS pa
|
||||||
|
INNER JOIN products AS p ON p.id = pa.product_id
|
||||||
|
WHERE p.client_id = :client_id
|
||||||
|
AND pa.campaign_id = :campaign_id';
|
||||||
|
|
||||||
|
if ( $ad_group_id > 0 )
|
||||||
|
{
|
||||||
|
$sql .= ' AND pa.ad_group_id = :ad_group_id';
|
||||||
|
$params[':ad_group_id'] = $ad_group_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= '
|
||||||
|
GROUP BY p.id, p.offer_id, p.name, p.title
|
||||||
|
HAVING COALESCE( SUM( pa.impressions_30 ), 0 ) = 0
|
||||||
|
ORDER BY COALESCE( NULLIF( TRIM( p.title ), \'\' ), NULLIF( TRIM( p.name ), \'\' ), p.offer_id ) ASC, p.id ASC
|
||||||
|
LIMIT ' . $limit;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
$statement = $mdb -> query( $sql, $params );
|
||||||
|
if ( !$statement )
|
||||||
|
{
|
||||||
|
error_log( '[products] get_products_without_impressions_30 query returned no statement.' );
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = $statement -> fetchAll( \PDO::FETCH_ASSOC );
|
||||||
|
return is_array( $rows ) ? $rows : [];
|
||||||
|
}
|
||||||
|
catch ( \Throwable $e )
|
||||||
|
{
|
||||||
|
error_log( '[products] get_products_without_impressions_30 query error: ' . $e -> getMessage() );
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static public function get_min_roas( $product_id )
|
static public function get_min_roas( $product_id )
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
@@ -68,13 +230,13 @@ class Products
|
|||||||
|
|
||||||
if ( $campaign_id > 0 )
|
if ( $campaign_id > 0 )
|
||||||
{
|
{
|
||||||
$sql .= ' AND pt.campaign_id = :campaign_id';
|
$sql .= ' AND pa.campaign_id = :campaign_id';
|
||||||
$params[':campaign_id'] = $campaign_id;
|
$params[':campaign_id'] = $campaign_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( $ad_group_id > 0 )
|
if ( $ad_group_id > 0 )
|
||||||
{
|
{
|
||||||
$sql .= ' AND pt.ad_group_id = :ad_group_id';
|
$sql .= ' AND pa.ad_group_id = :ad_group_id';
|
||||||
$params[':ad_group_id'] = $ad_group_id;
|
$params[':ad_group_id'] = $ad_group_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,44 +274,44 @@ class Products
|
|||||||
p.id AS product_id,
|
p.id AS product_id,
|
||||||
p.offer_id,
|
p.offer_id,
|
||||||
p.min_roas,
|
p.min_roas,
|
||||||
pt.campaign_id,
|
pa.campaign_id,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT( DISTINCT pt.campaign_id ) > 1 THEN \'--- wiele kampanii ---\'
|
WHEN COUNT( DISTINCT pa.campaign_id ) > 1 THEN \'--- wiele kampanii ---\'
|
||||||
ELSE COALESCE( MAX( c.campaign_name ), \'--- brak kampanii ---\' )
|
ELSE COALESCE( MAX( c.campaign_name ), \'--- brak kampanii ---\' )
|
||||||
END AS campaign_name,
|
END AS campaign_name,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT( DISTINCT pt.ad_group_id ) > 1 THEN \'--- wiele grup reklam ---\'
|
WHEN COUNT( DISTINCT pa.ad_group_id ) > 1 THEN \'--- wiele grup reklam ---\'
|
||||||
WHEN MAX( pt.ad_group_id ) = 0 THEN \'PMax (bez grup reklam)\'
|
WHEN MAX( pa.ad_group_id ) = 0 THEN \'PMax (bez grup reklam)\'
|
||||||
ELSE COALESCE( MAX( ag.ad_group_name ), \'--- brak grupy reklam ---\' )
|
ELSE COALESCE( MAX( ag.ad_group_name ), \'--- brak grupy reklam ---\' )
|
||||||
END AS ad_group_name,
|
END AS ad_group_name,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT( DISTINCT pt.ad_group_id ) = 1 THEN MAX( pt.ad_group_id )
|
WHEN COUNT( DISTINCT pa.ad_group_id ) = 1 THEN MAX( pa.ad_group_id )
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END AS ad_group_id,
|
END AS ad_group_id,
|
||||||
MAX( pt.name ) AS name,
|
COALESCE( NULLIF( TRIM( p.title ), \'\' ), NULLIF( TRIM( p.name ), \'\' ), p.offer_id ) AS name,
|
||||||
SUM( pt.impressions ) AS impressions,
|
SUM( pa.impressions_all_time ) AS impressions,
|
||||||
SUM( pt.impressions_30 ) AS impressions_30,
|
SUM( pa.impressions_30 ) AS impressions_30,
|
||||||
SUM( pt.clicks ) AS clicks,
|
SUM( pa.clicks_all_time ) AS clicks,
|
||||||
SUM( pt.clicks_30 ) AS clicks_30,
|
SUM( pa.clicks_30 ) AS clicks_30,
|
||||||
CASE
|
CASE
|
||||||
WHEN SUM( pt.impressions ) > 0 THEN ROUND( SUM( pt.clicks ) / SUM( pt.impressions ) * 100, 2 )
|
WHEN SUM( pa.impressions_all_time ) > 0 THEN ROUND( SUM( pa.clicks_all_time ) / SUM( pa.impressions_all_time ) * 100, 2 )
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END AS ctr,
|
END AS ctr,
|
||||||
SUM( pt.cost ) AS cost,
|
SUM( pa.cost_all_time ) AS cost,
|
||||||
CASE
|
CASE
|
||||||
WHEN SUM( pt.clicks ) > 0 THEN ROUND( SUM( pt.cost ) / SUM( pt.clicks ), 6 )
|
WHEN SUM( pa.clicks_all_time ) > 0 THEN ROUND( SUM( pa.cost_all_time ) / SUM( pa.clicks_all_time ), 6 )
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END AS cpc,
|
END AS cpc,
|
||||||
SUM( pt.conversions ) AS conversions,
|
SUM( pa.conversions_all_time ) AS conversions,
|
||||||
SUM( pt.conversions_value ) AS conversions_value,
|
SUM( pa.conversion_value_all_time ) AS conversions_value,
|
||||||
CASE
|
CASE
|
||||||
WHEN SUM( pt.cost ) > 0 THEN ROUND( SUM( pt.conversions_value ) / SUM( pt.cost ) * 100, 2 )
|
WHEN SUM( pa.cost_all_time ) > 0 THEN ROUND( SUM( pa.conversion_value_all_time ) / SUM( pa.cost_all_time ) * 100, 2 )
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END AS roas
|
END AS roas
|
||||||
FROM products_temp AS pt
|
FROM products_aggregate AS pa
|
||||||
INNER JOIN products AS p ON p.id = pt.product_id
|
INNER JOIN products AS p ON p.id = pa.product_id
|
||||||
LEFT JOIN campaigns AS c ON c.id = pt.campaign_id
|
LEFT JOIN campaigns AS c ON c.id = pa.campaign_id
|
||||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id
|
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pa.ad_group_id
|
||||||
WHERE p.client_id = :client_id';
|
WHERE p.client_id = :client_id';
|
||||||
|
|
||||||
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
||||||
@@ -157,7 +319,8 @@ class Products
|
|||||||
if ( $search )
|
if ( $search )
|
||||||
{
|
{
|
||||||
$sql .= ' AND (
|
$sql .= ' AND (
|
||||||
pt.name LIKE :search
|
p.name LIKE :search
|
||||||
|
OR p.title LIKE :search
|
||||||
OR p.offer_id LIKE :search
|
OR p.offer_id LIKE :search
|
||||||
OR c.campaign_name LIKE :search
|
OR c.campaign_name LIKE :search
|
||||||
OR ag.ad_group_name LIKE :search
|
OR ag.ad_group_name LIKE :search
|
||||||
@@ -165,7 +328,7 @@ class Products
|
|||||||
$params[':search'] = '%' . $search . '%';
|
$params[':search'] = '%' . $search . '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql .= ' GROUP BY p.id, p.offer_id, p.min_roas, pt.campaign_id';
|
$sql .= ' GROUP BY p.id, p.offer_id, p.min_roas, pa.campaign_id, p.name, p.title';
|
||||||
$sql .= ' ORDER BY ' . $order_sql . ' ' . $order_dir . ', product_id DESC LIMIT ' . $start . ', ' . $limit;
|
$sql .= ' ORDER BY ' . $order_sql . ' ' . $order_dir . ', product_id DESC LIMIT ' . $start . ', ' . $limit;
|
||||||
|
|
||||||
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
|
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||||
@@ -177,20 +340,27 @@ class Products
|
|||||||
|
|
||||||
$params = [ ':client_id' => $client_id ];
|
$params = [ ':client_id' => $client_id ];
|
||||||
|
|
||||||
$sql = 'SELECT MIN( p.min_roas ) AS min_roas, MAX( pt.roas ) AS max_roas
|
$sql = 'SELECT MIN( p.min_roas ) AS min_roas,
|
||||||
FROM products_temp AS pt
|
MAX(
|
||||||
INNER JOIN products AS p ON p.id = pt.product_id
|
CASE
|
||||||
LEFT JOIN campaigns AS c ON c.id = pt.campaign_id
|
WHEN COALESCE( pa.cost_all_time, 0 ) > 0 THEN ROUND( COALESCE( pa.conversion_value_all_time, 0 ) / pa.cost_all_time * 100, 2 )
|
||||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id
|
ELSE 0
|
||||||
|
END
|
||||||
|
) AS max_roas
|
||||||
|
FROM products_aggregate AS pa
|
||||||
|
INNER JOIN products AS p ON p.id = pa.product_id
|
||||||
|
LEFT JOIN campaigns AS c ON c.id = pa.campaign_id
|
||||||
|
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pa.ad_group_id
|
||||||
WHERE p.client_id = :client_id
|
WHERE p.client_id = :client_id
|
||||||
AND pt.conversions > 10';
|
AND pa.conversions_all_time > 10';
|
||||||
|
|
||||||
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
||||||
|
|
||||||
if ( $search )
|
if ( $search )
|
||||||
{
|
{
|
||||||
$sql .= ' AND (
|
$sql .= ' AND (
|
||||||
pt.name LIKE :search
|
p.name LIKE :search
|
||||||
|
OR p.title LIKE :search
|
||||||
OR p.offer_id LIKE :search
|
OR p.offer_id LIKE :search
|
||||||
OR c.campaign_name LIKE :search
|
OR c.campaign_name LIKE :search
|
||||||
OR ag.ad_group_name LIKE :search
|
OR ag.ad_group_name LIKE :search
|
||||||
@@ -213,11 +383,11 @@ class Products
|
|||||||
$params = [ ':client_id' => (int) $client_id ];
|
$params = [ ':client_id' => (int) $client_id ];
|
||||||
$sql = 'SELECT COUNT(0)
|
$sql = 'SELECT COUNT(0)
|
||||||
FROM (
|
FROM (
|
||||||
SELECT p.id, pt.campaign_id
|
SELECT p.id, pa.campaign_id
|
||||||
FROM products_temp AS pt
|
FROM products_aggregate AS pa
|
||||||
INNER JOIN products AS p ON p.id = pt.product_id
|
INNER JOIN products AS p ON p.id = pa.product_id
|
||||||
LEFT JOIN campaigns AS c ON c.id = pt.campaign_id
|
LEFT JOIN campaigns AS c ON c.id = pa.campaign_id
|
||||||
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pt.ad_group_id
|
LEFT JOIN campaign_ad_groups AS ag ON ag.id = pa.ad_group_id
|
||||||
WHERE p.client_id = :client_id';
|
WHERE p.client_id = :client_id';
|
||||||
|
|
||||||
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
||||||
@@ -225,7 +395,8 @@ class Products
|
|||||||
if ( $search )
|
if ( $search )
|
||||||
{
|
{
|
||||||
$sql .= ' AND (
|
$sql .= ' AND (
|
||||||
pt.name LIKE :search
|
p.name LIKE :search
|
||||||
|
OR p.title LIKE :search
|
||||||
OR p.offer_id LIKE :search
|
OR p.offer_id LIKE :search
|
||||||
OR c.campaign_name LIKE :search
|
OR c.campaign_name LIKE :search
|
||||||
OR ag.ad_group_name LIKE :search
|
OR ag.ad_group_name LIKE :search
|
||||||
@@ -233,7 +404,7 @@ class Products
|
|||||||
$params[':search'] = '%' . $search . '%';
|
$params[':search'] = '%' . $search . '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
$sql .= ' GROUP BY p.id, pt.campaign_id
|
$sql .= ' GROUP BY p.id, pa.campaign_id
|
||||||
) AS grouped_rows';
|
) AS grouped_rows';
|
||||||
|
|
||||||
return $mdb -> query( $sql, $params ) -> fetchColumn();
|
return $mdb -> query( $sql, $params ) -> fetchColumn();
|
||||||
@@ -248,30 +419,30 @@ class Products
|
|||||||
p.offer_id,
|
p.offer_id,
|
||||||
p.name,
|
p.name,
|
||||||
p.min_roas,
|
p.min_roas,
|
||||||
COALESCE( SUM( pt.impressions ), 0 ) AS impressions,
|
COALESCE( SUM( pa.impressions_all_time ), 0 ) AS impressions,
|
||||||
COALESCE( SUM( pt.impressions_30 ), 0 ) AS impressions_30,
|
COALESCE( SUM( pa.impressions_30 ), 0 ) AS impressions_30,
|
||||||
COALESCE( SUM( pt.clicks ), 0 ) AS clicks,
|
COALESCE( SUM( pa.clicks_all_time ), 0 ) AS clicks,
|
||||||
COALESCE( SUM( pt.clicks_30 ), 0 ) AS clicks_30,
|
COALESCE( SUM( pa.clicks_30 ), 0 ) AS clicks_30,
|
||||||
CASE
|
CASE
|
||||||
WHEN COALESCE( SUM( pt.impressions ), 0 ) > 0
|
WHEN COALESCE( SUM( pa.impressions_all_time ), 0 ) > 0
|
||||||
THEN ROUND( COALESCE( SUM( pt.clicks ), 0 ) / COALESCE( SUM( pt.impressions ), 0 ) * 100, 2 )
|
THEN ROUND( COALESCE( SUM( pa.clicks_all_time ), 0 ) / COALESCE( SUM( pa.impressions_all_time ), 0 ) * 100, 2 )
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END AS ctr,
|
END AS ctr,
|
||||||
COALESCE( SUM( pt.cost ), 0 ) AS cost,
|
COALESCE( SUM( pa.cost_all_time ), 0 ) AS cost,
|
||||||
CASE
|
CASE
|
||||||
WHEN COALESCE( SUM( pt.clicks ), 0 ) > 0
|
WHEN COALESCE( SUM( pa.clicks_all_time ), 0 ) > 0
|
||||||
THEN ROUND( COALESCE( SUM( pt.cost ), 0 ) / COALESCE( SUM( pt.clicks ), 0 ), 6 )
|
THEN ROUND( COALESCE( SUM( pa.cost_all_time ), 0 ) / COALESCE( SUM( pa.clicks_all_time ), 0 ), 6 )
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END AS cpc,
|
END AS cpc,
|
||||||
COALESCE( SUM( pt.conversions ), 0 ) AS conversions,
|
COALESCE( SUM( pa.conversions_all_time ), 0 ) AS conversions,
|
||||||
COALESCE( SUM( pt.conversions_value ), 0 ) AS conversions_value,
|
COALESCE( SUM( pa.conversion_value_all_time ), 0 ) AS conversions_value,
|
||||||
CASE
|
CASE
|
||||||
WHEN COALESCE( SUM( pt.cost ), 0 ) > 0
|
WHEN COALESCE( SUM( pa.cost_all_time ), 0 ) > 0
|
||||||
THEN ROUND( COALESCE( SUM( pt.conversions_value ), 0 ) / COALESCE( SUM( pt.cost ), 0 ) * 100, 2 )
|
THEN ROUND( COALESCE( SUM( pa.conversion_value_all_time ), 0 ) / COALESCE( SUM( pa.cost_all_time ), 0 ) * 100, 2 )
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END AS roas
|
END AS roas
|
||||||
FROM products AS p
|
FROM products AS p
|
||||||
LEFT JOIN products_temp AS pt ON pt.product_id = p.id
|
LEFT JOIN products_aggregate AS pa ON pa.product_id = p.id
|
||||||
WHERE p.id = :pid
|
WHERE p.id = :pid
|
||||||
GROUP BY p.id, p.offer_id, p.name, p.min_roas',
|
GROUP BY p.id, p.offer_id, p.name, p.min_roas',
|
||||||
[ ':pid' => $product_id ]
|
[ ':pid' => $product_id ]
|
||||||
@@ -281,7 +452,21 @@ class Products
|
|||||||
static public function get_product_data( $product_id, $field )
|
static public function get_product_data( $product_id, $field )
|
||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
return $mdb -> get( 'products_data', $field, [ 'product_id' => $product_id ] );
|
|
||||||
|
$product_id = (int) $product_id;
|
||||||
|
$field = trim( (string) $field );
|
||||||
|
|
||||||
|
if ( $product_id <= 0 || $field === '' )
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !self::is_product_core_field( $field ) )
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $mdb -> get( 'products', $field, [ 'id' => $product_id ] );
|
||||||
}
|
}
|
||||||
|
|
||||||
static public function get_product_merchant_context( $product_id )
|
static public function get_product_merchant_context( $product_id )
|
||||||
@@ -459,10 +644,36 @@ class Products
|
|||||||
{
|
{
|
||||||
global $mdb;
|
global $mdb;
|
||||||
|
|
||||||
if ( !$mdb -> count( 'products_data', [ 'product_id' => $product_id ] ) )
|
$product_id = (int) $product_id;
|
||||||
$result = $mdb -> insert( 'products_data', [ 'product_id' => $product_id, $field => $value ] );
|
$field = trim( (string) $field );
|
||||||
else
|
|
||||||
$result = $mdb -> update( 'products_data', [ $field => $value ], [ 'product_id' => $product_id ] );
|
if ( $product_id <= 0 || $field === '' )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = false;
|
||||||
|
if ( self::is_product_core_field( $field ) )
|
||||||
|
{
|
||||||
|
$update_data = [ $field => $value ];
|
||||||
|
if ( $field === 'product_url' )
|
||||||
|
{
|
||||||
|
$product_url = trim( (string) $value );
|
||||||
|
if ( $product_url === '' || in_array( strtolower( $product_url ), [ '0', '-', 'null' ], true ) )
|
||||||
|
{
|
||||||
|
$update_data['product_url'] = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$update_data['merchant_url_not_found'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$update_data['merchant_url_last_check'] = date( 'Y-m-d H:i:s' );
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $mdb -> update( 'products', $update_data, [ 'id' => $product_id ] );
|
||||||
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,7 +1021,7 @@ class Products
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope_exists = (int) $mdb -> count( 'products_temp', [
|
$scope_exists = (int) $mdb -> count( 'products_aggregate', [
|
||||||
'AND' => [
|
'AND' => [
|
||||||
'product_id' => $product_id,
|
'product_id' => $product_id,
|
||||||
'campaign_id' => $campaign_id,
|
'campaign_id' => $campaign_id,
|
||||||
@@ -820,34 +1031,28 @@ class Products
|
|||||||
|
|
||||||
if ( $scope_exists )
|
if ( $scope_exists )
|
||||||
{
|
{
|
||||||
$mdb -> update( 'products_temp', [
|
|
||||||
'name' => $product['name']
|
|
||||||
], [
|
|
||||||
'AND' => [
|
|
||||||
'product_id' => $product_id,
|
|
||||||
'campaign_id' => $campaign_id,
|
|
||||||
'ad_group_id' => $ad_group_id
|
|
||||||
]
|
|
||||||
] );
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $mdb -> insert( 'products_temp', [
|
return $mdb -> insert( 'products_aggregate', [
|
||||||
'product_id' => $product_id,
|
'product_id' => $product_id,
|
||||||
'campaign_id' => $campaign_id,
|
'campaign_id' => $campaign_id,
|
||||||
'ad_group_id' => $ad_group_id,
|
'ad_group_id' => $ad_group_id,
|
||||||
'name' => $product['name'],
|
|
||||||
'impressions' => 0,
|
|
||||||
'impressions_30' => 0,
|
'impressions_30' => 0,
|
||||||
'clicks' => 0,
|
|
||||||
'clicks_30' => 0,
|
'clicks_30' => 0,
|
||||||
'ctr' => 0,
|
'ctr_30' => 0,
|
||||||
'cost' => 0,
|
'cost_30' => 0,
|
||||||
'conversions' => 0,
|
'conversions_30' => 0,
|
||||||
'conversions_value' => 0,
|
'conversion_value_30' => 0,
|
||||||
'cpc' => 0,
|
'roas_30' => 0,
|
||||||
'roas' => 0
|
'impressions_all_time' => 0,
|
||||||
|
'clicks_all_time' => 0,
|
||||||
|
'ctr_all_time' => 0,
|
||||||
|
'cost_all_time' => 0,
|
||||||
|
'conversions_all_time' => 0,
|
||||||
|
'conversion_value_all_time' => 0,
|
||||||
|
'roas_all_time' => 0,
|
||||||
|
'date_sync' => date( 'Y-m-d' )
|
||||||
] );
|
] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1023,6 +1023,58 @@ class GoogleAdsApi
|
|||||||
return $this -> mutate( $customer_id, [ $operation ] ) !== false;
|
return $this -> mutate( $customer_id, [ $operation ] ) !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function remove_ad_group( $customer_id, $ad_group_id )
|
||||||
|
{
|
||||||
|
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
|
||||||
|
$ad_group_id = (int) $ad_group_id;
|
||||||
|
|
||||||
|
if ( $customer_id === '' || $ad_group_id <= 0 )
|
||||||
|
{
|
||||||
|
self::set_setting( 'google_ads_last_error', 'Brak danych do usuniecia grupy reklam.' );
|
||||||
|
return [ 'success' => false, 'removed' => 0 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
$resource_name = 'customers/' . $customer_id . '/adGroups/' . $ad_group_id;
|
||||||
|
$operation = [
|
||||||
|
'adGroupOperation' => [
|
||||||
|
'remove' => $resource_name
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$mutate_result = $this -> mutate( $customer_id, [ $operation ] );
|
||||||
|
|
||||||
|
if ( $mutate_result === false )
|
||||||
|
{
|
||||||
|
$last_error = (string) self::get_setting( 'google_ads_last_error' );
|
||||||
|
$is_not_found = stripos( $last_error, 'NOT_FOUND' ) !== false
|
||||||
|
|| stripos( $last_error, 'RESOURCE_NOT_FOUND' ) !== false;
|
||||||
|
|
||||||
|
if ( $is_not_found )
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'removed' => 0,
|
||||||
|
'not_found' => true,
|
||||||
|
'resource_name' => $resource_name
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'removed' => 0,
|
||||||
|
'error' => $last_error,
|
||||||
|
'resource_name' => $resource_name
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'removed' => 1,
|
||||||
|
'response' => $mutate_result,
|
||||||
|
'resource_name' => $resource_name
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function get_root_listing_group_resource_name( $customer_id, $ad_group_id )
|
private function get_root_listing_group_resource_name( $customer_id, $ad_group_id )
|
||||||
{
|
{
|
||||||
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
|
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
|
||||||
@@ -2468,8 +2520,11 @@ class GoogleAdsApi
|
|||||||
return array_values( $campaigns );
|
return array_values( $campaigns );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_ad_groups_30_days( $customer_id )
|
public function get_ad_groups_30_days( $customer_id, $as_of_date = null )
|
||||||
{
|
{
|
||||||
|
$as_of_date = $as_of_date ? date( 'Y-m-d', strtotime( $as_of_date ) ) : date( 'Y-m-d' );
|
||||||
|
$date_from = date( 'Y-m-d', strtotime( '-29 days', strtotime( $as_of_date ) ) );
|
||||||
|
|
||||||
$gaql = "SELECT "
|
$gaql = "SELECT "
|
||||||
. "campaign.id, "
|
. "campaign.id, "
|
||||||
. "ad_group.id, "
|
. "ad_group.id, "
|
||||||
@@ -2482,7 +2537,8 @@ class GoogleAdsApi
|
|||||||
. "FROM ad_group "
|
. "FROM ad_group "
|
||||||
. "WHERE campaign.status = 'ENABLED' "
|
. "WHERE campaign.status = 'ENABLED' "
|
||||||
. "AND ad_group.status = 'ENABLED' "
|
. "AND ad_group.status = 'ENABLED' "
|
||||||
. "AND segments.date DURING LAST_30_DAYS";
|
. "AND segments.date >= '" . $date_from . "' "
|
||||||
|
. "AND segments.date <= '" . $as_of_date . "'";
|
||||||
|
|
||||||
$results = $this -> search_stream( $customer_id, $gaql );
|
$results = $this -> search_stream( $customer_id, $gaql );
|
||||||
if ( $results === false ) return false;
|
if ( $results === false ) return false;
|
||||||
@@ -2490,7 +2546,7 @@ class GoogleAdsApi
|
|||||||
return $this -> aggregate_ad_groups( $results );
|
return $this -> aggregate_ad_groups( $results );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_ad_groups_all_time( $customer_id )
|
public function get_ad_groups_all_time( $customer_id, $as_of_date = null )
|
||||||
{
|
{
|
||||||
$gaql = "SELECT "
|
$gaql = "SELECT "
|
||||||
. "campaign.id, "
|
. "campaign.id, "
|
||||||
@@ -2505,7 +2561,24 @@ class GoogleAdsApi
|
|||||||
. "WHERE campaign.status = 'ENABLED' "
|
. "WHERE campaign.status = 'ENABLED' "
|
||||||
. "AND ad_group.status = 'ENABLED'";
|
. "AND ad_group.status = 'ENABLED'";
|
||||||
|
|
||||||
$results = $this -> search_stream( $customer_id, $gaql );
|
$results = false;
|
||||||
|
if ( $as_of_date )
|
||||||
|
{
|
||||||
|
$as_of_date = date( 'Y-m-d', strtotime( $as_of_date ) );
|
||||||
|
$gaql_with_date = $gaql . " AND segments.date <= '" . $as_of_date . "'";
|
||||||
|
$results = $this -> search_stream( $customer_id, $gaql_with_date );
|
||||||
|
|
||||||
|
// Fallback gdy konto nie akceptuje filtra daty na all-time.
|
||||||
|
if ( $results === false )
|
||||||
|
{
|
||||||
|
$results = $this -> search_stream( $customer_id, $gaql );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$results = $this -> search_stream( $customer_id, $gaql );
|
||||||
|
}
|
||||||
|
|
||||||
if ( $results === false ) return false;
|
if ( $results === false ) return false;
|
||||||
|
|
||||||
return $this -> aggregate_ad_groups( $results );
|
return $this -> aggregate_ad_groups( $results );
|
||||||
@@ -2688,6 +2761,72 @@ class GoogleAdsApi
|
|||||||
return array_values( $scopes );
|
return array_values( $scopes );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function get_shopping_ad_group_offer_ids_from_shopping_product( $customer_id )
|
||||||
|
{
|
||||||
|
$gaql = "SELECT "
|
||||||
|
. "campaign.id, "
|
||||||
|
. "campaign.name, "
|
||||||
|
. "campaign.status, "
|
||||||
|
. "campaign.advertising_channel_type, "
|
||||||
|
. "ad_group.id, "
|
||||||
|
. "ad_group.name, "
|
||||||
|
. "ad_group.status, "
|
||||||
|
. "shopping_product.item_id "
|
||||||
|
. "FROM shopping_product "
|
||||||
|
. "WHERE campaign.status != 'REMOVED' "
|
||||||
|
. "AND ad_group.status != 'REMOVED' "
|
||||||
|
. "AND campaign.advertising_channel_type = 'SHOPPING' "
|
||||||
|
. "AND segments.date DURING LAST_30_DAYS";
|
||||||
|
|
||||||
|
$results = $this -> search_stream( $customer_id, $gaql );
|
||||||
|
if ( $results === false )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !is_array( $results ) )
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$scopes = [];
|
||||||
|
|
||||||
|
foreach ( $results as $row )
|
||||||
|
{
|
||||||
|
$campaign_id = (int) ( $row['campaign']['id'] ?? 0 );
|
||||||
|
$ad_group_id = (int) ( $row['adGroup']['id'] ?? 0 );
|
||||||
|
$offer_id = trim( (string) ( $row['shoppingProduct']['itemId'] ?? '' ) );
|
||||||
|
|
||||||
|
if ( $campaign_id <= 0 || $ad_group_id <= 0 || $offer_id === '' )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope_key = $campaign_id . '|' . $ad_group_id;
|
||||||
|
|
||||||
|
if ( !isset( $scopes[ $scope_key ] ) )
|
||||||
|
{
|
||||||
|
$scopes[ $scope_key ] = [
|
||||||
|
'campaign_id' => $campaign_id,
|
||||||
|
'campaign_name' => trim( (string) ( $row['campaign']['name'] ?? '' ) ),
|
||||||
|
'ad_group_id' => $ad_group_id,
|
||||||
|
'ad_group_name' => trim( (string) ( $row['adGroup']['name'] ?? '' ) ),
|
||||||
|
'offer_ids' => []
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$scopes[ $scope_key ]['offer_ids'][ $offer_id ] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $scopes as &$scope )
|
||||||
|
{
|
||||||
|
$scope['offer_ids'] = array_values( array_keys( (array) $scope['offer_ids'] ) );
|
||||||
|
}
|
||||||
|
unset( $scope );
|
||||||
|
|
||||||
|
return array_values( $scopes );
|
||||||
|
}
|
||||||
|
|
||||||
public function get_shopping_ad_group_offer_ids_from_performance( $customer_id )
|
public function get_shopping_ad_group_offer_ids_from_performance( $customer_id )
|
||||||
{
|
{
|
||||||
$gaql = "SELECT "
|
$gaql = "SELECT "
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ $settings['email_password'] = 'ProjectPro2025!';
|
|||||||
|
|
||||||
$settings['cron_products_clients_per_run'] = 1;
|
$settings['cron_products_clients_per_run'] = 1;
|
||||||
$settings['cron_campaigns_clients_per_run'] = 1;
|
$settings['cron_campaigns_clients_per_run'] = 1;
|
||||||
$settings['cron_products_urls_limit_per_client'] = 10;
|
$settings['cron_products_urls_limit_per_client'] = 100;
|
||||||
$settings['google_ads_conversion_window_days'] = 7;
|
$settings['google_ads_conversion_window_days'] = 7;
|
||||||
|
|||||||
280
docs/PLAN.md
280
docs/PLAN.md
@@ -1,280 +0,0 @@
|
|||||||
# adsPRO - System Zarządzania Reklamami Google ADS & Facebook ADS
|
|
||||||
|
|
||||||
## Opis projektu
|
|
||||||
adsPRO to narzędzie webowe (PHP) do zarządzania i automatyzacji kampanii reklamowych Google ADS (priorytet) oraz Facebook ADS (planowane). System umożliwia monitorowanie kampanii, zarządzanie produktami, analizę wydajności (ROAS, CTR, CPC) oraz automatyczne etykietowanie produktów (bestsellery, zombie itp.).
|
|
||||||
|
|
||||||
**URL:** https://adspro.projectpro.pl
|
|
||||||
**Hosting:** Hostido (shared hosting)
|
|
||||||
|
|
||||||
## Stack technologiczny
|
|
||||||
- **PHP 8.x** - czyste PHP z własną strukturą MVC (bez frameworka)
|
|
||||||
- **MySQL/MariaDB** - baza danych (Medoo ORM)
|
|
||||||
- **Google ADS API** - pobieranie danych kampanii i produktów (CRON)
|
|
||||||
- **Facebook ADS API** - planowane w przyszłości
|
|
||||||
- **CRON** - automatyczna synchronizacja danych
|
|
||||||
- **SCSS** - stylowanie (kompilacja do CSS)
|
|
||||||
- **jQuery 3.6** - interaktywność frontend
|
|
||||||
- **DataTables 2.1.7** - tabele z sortowaniem, filtrowaniem, paginacją
|
|
||||||
- **Highcharts** - wykresy wydajności
|
|
||||||
- **Select2** - zaawansowane selecty
|
|
||||||
- **Font Awesome** - ikony
|
|
||||||
|
|
||||||
## Struktura katalogów (nowa)
|
|
||||||
|
|
||||||
```
|
|
||||||
public_html/
|
|
||||||
├── index.php # Front controller + nowy router
|
|
||||||
├── .htaccess # Rewrite rules
|
|
||||||
├── .env # Konfiguracja (przyszłość - migracja z config.php)
|
|
||||||
├── config.php # Konfiguracja DB (obecna)
|
|
||||||
├── ajax.php # Ajax handler
|
|
||||||
├── api.php # API handler (Google ADS webhook)
|
|
||||||
├── cron.php # CRON handler
|
|
||||||
├── robots.txt
|
|
||||||
├── layout/
|
|
||||||
│ ├── favicon.png
|
|
||||||
│ ├── style.scss # Główne style (SCSS)
|
|
||||||
│ └── style.css # Skompilowane style
|
|
||||||
├── libraries/ # Biblioteki zewnętrzne
|
|
||||||
│ ├── medoo/
|
|
||||||
│ ├── phpmailer/
|
|
||||||
│ ├── select2/
|
|
||||||
│ ├── jquery-confirm/
|
|
||||||
│ ├── functions.js # Globalne funkcje JS
|
|
||||||
│ └── framework/ # Framework UI (skin, pluginy)
|
|
||||||
├── autoload/ # Kod PHP (MVC)
|
|
||||||
│ ├── class.S.php # Helper: sesje, requesty, narzędzia
|
|
||||||
│ ├── class.Tpl.php # Template engine
|
|
||||||
│ ├── class.Cache.php # Cache
|
|
||||||
│ ├── class.DbModel.php # Bazowy model DB
|
|
||||||
│ ├── class.Html.php # HTML helper
|
|
||||||
│ ├── controls/ # Kontrolery
|
|
||||||
│ │ ├── class.Site.php # Router (do przebudowy)
|
|
||||||
│ │ ├── class.Users.php # Logowanie, ustawienia
|
|
||||||
│ │ ├── class.Dashboard.php # NOWY - Dashboard główny
|
|
||||||
│ │ ├── class.Campaigns.php # Kampanie Google ADS
|
|
||||||
│ │ ├── class.Products.php # Produkty
|
|
||||||
│ │ ├── class.Allegro.php # Import Allegro
|
|
||||||
│ │ ├── class.Reports.php # NOWY - Raporty i analityka
|
|
||||||
│ │ ├── class.Api.php # Endpointy API
|
|
||||||
│ │ └── class.Cron.php # CRON joby
|
|
||||||
│ ├── factory/ # Modele danych (DB queries)
|
|
||||||
│ │ ├── class.Users.php
|
|
||||||
│ │ ├── class.Campaigns.php
|
|
||||||
│ │ ├── class.Products.php
|
|
||||||
│ │ └── class.Cron.php
|
|
||||||
│ └── view/ # View helpers
|
|
||||||
│ ├── class.Site.php # Renderer layoutu
|
|
||||||
│ ├── class.Users.php
|
|
||||||
│ └── class.Cron.php
|
|
||||||
├── templates/ # Szablony PHP
|
|
||||||
│ ├── site/
|
|
||||||
│ │ ├── layout-logged.php # Layout z sidebar (PRZEBUDOWA)
|
|
||||||
│ │ └── layout-unlogged.php # Layout logowania (PRZEBUDOWA)
|
|
||||||
│ ├── auth/
|
|
||||||
│ │ └── login.php # NOWY ekran logowania
|
|
||||||
│ ├── dashboard/
|
|
||||||
│ │ └── index.php # NOWY dashboard
|
|
||||||
│ ├── campaigns/
|
|
||||||
│ │ └── main_view.php # Widok kampanii
|
|
||||||
│ ├── products/
|
|
||||||
│ │ ├── main_view.php # Lista produktów
|
|
||||||
│ │ └── product_history.php # Historia produktu
|
|
||||||
│ ├── allegro/
|
|
||||||
│ │ └── main_view.php # Import Allegro
|
|
||||||
│ ├── reports/ # NOWE
|
|
||||||
│ │ └── index.php # Raporty
|
|
||||||
│ ├── users/
|
|
||||||
│ │ ├── login-form.php # Stary login (do usunięcia)
|
|
||||||
│ │ └── settings.php # Ustawienia użytkownika
|
|
||||||
│ └── html/ # Komponenty HTML
|
|
||||||
│ ├── button.php
|
|
||||||
│ ├── input.php
|
|
||||||
│ ├── select.php
|
|
||||||
│ └── ...
|
|
||||||
├── tools/
|
|
||||||
│ └── google-taxonomy.php
|
|
||||||
├── tmp/
|
|
||||||
└── docs/
|
|
||||||
└── PLAN.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Nowy system routingu
|
|
||||||
|
|
||||||
### Zasada działania
|
|
||||||
Zamiast obecnego `?module=X&action=Y` → czyste URLe obsługiwane przez `.htaccess` + nowy router w `class.Site.php`.
|
|
||||||
|
|
||||||
### Mapa URL
|
|
||||||
|
|
||||||
| URL | Kontroler | Metoda | Opis |
|
|
||||||
|-----|-----------|--------|------|
|
|
||||||
| `/login` | Users | login_form | Ekran logowania |
|
|
||||||
| `/logout` | Users | logout | Wylogowanie |
|
|
||||||
| `/` | Dashboard | index | Dashboard główny |
|
|
||||||
| `/campaigns` | Campaigns | main_view | Lista kampanii |
|
|
||||||
| `/campaigns/history/{id}` | Campaigns | history | Historia kampanii |
|
|
||||||
| `/products` | Products | main_view | Lista produktów |
|
|
||||||
| `/products/history/{id}` | Products | product_history | Historia produktu |
|
|
||||||
| `/allegro` | Allegro | main_view | Import Allegro |
|
|
||||||
| `/reports` | Reports | index | Raporty |
|
|
||||||
| `/settings` | Users | settings | Ustawienia konta |
|
|
||||||
| `/api/*` | Api | * | Endpointy API |
|
|
||||||
| `/cron/*` | Cron | * | CRON joby |
|
|
||||||
|
|
||||||
### Nowy .htaccess
|
|
||||||
```apache
|
|
||||||
RewriteEngine On
|
|
||||||
RewriteBase /
|
|
||||||
|
|
||||||
# Statyczne zasoby - pomijaj
|
|
||||||
RewriteCond %{REQUEST_URI} ^/(libraries|layout|upload|temp)/ [NC]
|
|
||||||
RewriteRule ^ - [L]
|
|
||||||
|
|
||||||
# Wszystko inne → index.php
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^(.*)$ index.php [L,QSA]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Nowy router (class.Site.php)
|
|
||||||
```php
|
|
||||||
// Parsowanie URL z $_SERVER['REQUEST_URI']
|
|
||||||
// Mapowanie: /segment1/segment2/segment3 → kontroler/akcja/parametry
|
|
||||||
// Fallback na dashboard dla zalogowanych, login dla niezalogowanych
|
|
||||||
```
|
|
||||||
|
|
||||||
## Główne funkcje
|
|
||||||
|
|
||||||
### 1. Nowy ekran logowania
|
|
||||||
- Nowoczesny design: podzielony ekran (lewa strona - branding/grafika, prawa - formularz)
|
|
||||||
- Logo "adsPRO" z subtitlem
|
|
||||||
- Pola: email + hasło
|
|
||||||
- Checkbox "Zapamiętaj mnie"
|
|
||||||
- Walidacja AJAX
|
|
||||||
- Animacje przejścia
|
|
||||||
- Responsywność (mobile: tylko formularz)
|
|
||||||
|
|
||||||
### 2. Nowy layout z menu bocznym (sidebar)
|
|
||||||
- **Sidebar (lewa strona, 260px):**
|
|
||||||
- Logo "adsPRO" na górze
|
|
||||||
- Menu nawigacyjne z ikonami Font Awesome:
|
|
||||||
- 📊 Dashboard (`/`)
|
|
||||||
- 📢 Kampanie (`/campaigns`)
|
|
||||||
- 📦 Produkty (`/products`)
|
|
||||||
- 📥 Allegro import (`/allegro`)
|
|
||||||
- 📈 Raporty (`/reports`)
|
|
||||||
- ⚙️ Ustawienia (`/settings`)
|
|
||||||
- Aktywny element podświetlony
|
|
||||||
- Możliwość zwijania sidebar (collapsed → same ikony, 60px)
|
|
||||||
- Na dole: info o zalogowanym użytkowniku + przycisk wylogowania
|
|
||||||
- **Top bar (nad contentem):**
|
|
||||||
- Przycisk hamburger (toggle sidebar)
|
|
||||||
- Breadcrumbs (ścieżka nawigacji)
|
|
||||||
- Szybkie akcje / notyfikacje (przyszłość)
|
|
||||||
- **Content area:**
|
|
||||||
- Pełna szerokość minus sidebar
|
|
||||||
- Padding 25px
|
|
||||||
- Tło #F4F6F9 (jaśniejsze od obecnego)
|
|
||||||
|
|
||||||
### 3. Dashboard (NOWY)
|
|
||||||
- Kafelki podsumowujące (karty):
|
|
||||||
- Łączna liczba kampanii
|
|
||||||
- Łączna liczba produktów
|
|
||||||
- Średni ROAS (30 dni)
|
|
||||||
- Łączne wydatki (30 dni)
|
|
||||||
- Wykres trendu ROAS (ostatnie 30 dni)
|
|
||||||
- Lista ostatnio zmodyfikowanych kampanii
|
|
||||||
- Produkty wymagające uwagi (niski ROAS, zombie)
|
|
||||||
|
|
||||||
### 4. Zarządzanie kampaniami Google ADS
|
|
||||||
- Wybór klienta (select)
|
|
||||||
- Lista kampanii z metrykami (DataTables)
|
|
||||||
- Historia kampanii z wykresem Highcharts
|
|
||||||
- Metryki: ROAS, budżet, wydatki, wartość konwersji, strategia bidding
|
|
||||||
- Usuwanie kampanii i wpisów historii
|
|
||||||
- Komentarze do kampanii
|
|
||||||
|
|
||||||
### 5. Zarządzanie produktami
|
|
||||||
- Wybór klienta
|
|
||||||
- Konfiguracja min. ROAS dla bestsellerów
|
|
||||||
- Tabela produktów z metrykami:
|
|
||||||
- Wyświetlenia, kliknięcia, CTR, koszt, CPC
|
|
||||||
- Konwersje, wartość konwersji, ROAS
|
|
||||||
- Custom labels (bestseller/zombie/deleted/pla/paused)
|
|
||||||
- Edycja inline (min_roas, custom_label)
|
|
||||||
- Edycja produktu w modalu (tytuł, opis, kategoria Google)
|
|
||||||
- Historia produktu z wykresem
|
|
||||||
- Bulk delete zaznaczonych produktów
|
|
||||||
|
|
||||||
### 6. Import Allegro
|
|
||||||
- Upload pliku CSV
|
|
||||||
- Automatyczne mapowanie ofert
|
|
||||||
- Raport importu (dodane, zaktualizowane)
|
|
||||||
|
|
||||||
### 7. Raporty (NOWY - przyszłość)
|
|
||||||
- Raport wydajności kampanii
|
|
||||||
- Raport produktów (bestsellery vs zombie)
|
|
||||||
- Eksport do Excel
|
|
||||||
- Porównanie okresów
|
|
||||||
|
|
||||||
### 8. Ustawienia
|
|
||||||
- Dane konta (email)
|
|
||||||
- Zmiana hasła
|
|
||||||
- Konfiguracja Pushover (powiadomienia)
|
|
||||||
- Klucze API (przyszłość: Google ADS, Facebook ADS)
|
|
||||||
|
|
||||||
### 9. CRON - synchronizacja danych
|
|
||||||
- `cron_products` - synchronizacja produktów z Google ADS
|
|
||||||
- `cron_products_history_30` - historia 30-dniowa produktów
|
|
||||||
- `cron_xml` - generowanie XML
|
|
||||||
- `cron_phrases` - synchronizacja fraz
|
|
||||||
- `cron_phrases_history_30` - historia 30-dniowa fraz
|
|
||||||
|
|
||||||
## Plan implementacji
|
|
||||||
|
|
||||||
| Etap | Zakres | Priorytet | Pliki |
|
|
||||||
|------|--------|-----------|-------|
|
|
||||||
| **1. Nowy routing** | Przebudowa routera, nowy .htaccess, parsowanie czystych URL | 🔴 Wysoki | `.htaccess`, `index.php`, `controls/class.Site.php` |
|
|
||||||
| **2. Nowy layout (sidebar)** | Layout z bocznym menu, top bar, responsywność | 🔴 Wysoki | `templates/site/layout-logged.php`, `layout/style.scss` |
|
|
||||||
| **3. Nowy ekran logowania** | Nowoczesny split-screen login, nowy layout-unlogged | 🔴 Wysoki | `templates/site/layout-unlogged.php`, `templates/auth/login.php`, `layout/style.scss` |
|
|
||||||
| **4. Dashboard** | Nowa strona startowa z podsumowaniem | 🟡 Średni | `controls/class.Dashboard.php`, `templates/dashboard/index.php` |
|
|
||||||
| **5. Migracja kampanii** | Dostosowanie widoku kampanii do nowego routingu | 🟡 Średni | `controls/class.Campaigns.php`, `templates/campaigns/*` |
|
|
||||||
| **6. Migracja produktów** | Dostosowanie widoku produktów do nowego routingu | 🟡 Średni | `controls/class.Products.php`, `templates/products/*` |
|
|
||||||
| **7. Migracja Allegro** | Dostosowanie importu Allegro | 🟢 Niski | `controls/class.Allegro.php`, `templates/allegro/*` |
|
|
||||||
| **8. Moduł raportów** | Nowy moduł analityczny | 🟢 Niski | `controls/class.Reports.php`, `templates/reports/*` |
|
|
||||||
| **9. Facebook ADS** | Integracja z Facebook ADS API | 🔵 Przyszłość | Nowe kontrolery, factory, szablony |
|
|
||||||
|
|
||||||
## Kolorystyka i design
|
|
||||||
|
|
||||||
### Paleta kolorów
|
|
||||||
- **Primary (akcent):** `#6690F4` (niebieski - obecny)
|
|
||||||
- **Sidebar tło:** `#1E2A3A` (ciemny granat)
|
|
||||||
- **Sidebar tekst:** `#A8B7C7` (jasny szary)
|
|
||||||
- **Sidebar active:** `#6690F4` (primary)
|
|
||||||
- **Content tło:** `#F4F6F9` (jasnoszary)
|
|
||||||
- **Karty:** `#FFFFFF`
|
|
||||||
- **Tekst:** `#4E5E6A` (obecny)
|
|
||||||
- **Success:** `#57B951`
|
|
||||||
- **Danger:** `#CC0000`
|
|
||||||
- **Warning:** `#FF8C00`
|
|
||||||
|
|
||||||
### Typografia
|
|
||||||
- Font: Open Sans (obecny - zachowany)
|
|
||||||
- Rozmiar bazowy: 14px (sidebar), 15px (content)
|
|
||||||
|
|
||||||
## Bezpieczeństwo
|
|
||||||
- Hasła hashowane MD5 (obecne) → **TODO: migracja na bcrypt**
|
|
||||||
- Sesje PHP + cookie "zapamiętaj mnie"
|
|
||||||
- Prepared statements (Medoo ORM)
|
|
||||||
- htmlspecialchars() w szablonach
|
|
||||||
- **TODO: CSRF tokeny w formularzach**
|
|
||||||
- **TODO: migracja config.php → .env (z .htaccess deny)**
|
|
||||||
|
|
||||||
## Przyszłe rozszerzenia
|
|
||||||
- Facebook ADS API - zarządzanie kampaniami FB
|
|
||||||
- System powiadomień (Pushover + in-app)
|
|
||||||
- Wielojęzyczność (PL/EN)
|
|
||||||
- Role użytkowników (admin, manager, viewer)
|
|
||||||
- Automatyczne reguły (np. "jeśli ROAS < X → zmień label na zombie")
|
|
||||||
- Integracja z Google Merchant Center
|
|
||||||
- API REST do integracji z innymi systemami
|
|
||||||
@@ -1,44 +1,24 @@
|
|||||||
-- --------------------------------------------------------
|
-- Zrzut struktury tabela host700513_adspro.clients
|
||||||
-- Host: host700513.hostido.net.pl
|
CREATE TABLE IF NOT EXISTS `clients` (
|
||||||
-- Wersja serwera: 10.11.15-MariaDB-cll-lve - MariaDB Server
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
-- Serwer OS: Linux
|
`name` varchar(255) NOT NULL DEFAULT '0',
|
||||||
-- HeidiSQL Wersja: 12.6.0.6765
|
`google_ads_customer_id` varchar(20) DEFAULT NULL,
|
||||||
-- --------------------------------------------------------
|
`google_merchant_account_id` varchar(32) DEFAULT NULL,
|
||||||
|
`google_ads_start_date` date DEFAULT NULL,
|
||||||
|
`active` int(11) DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=48 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
/*!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` (
|
CREATE TABLE IF NOT EXISTS `campaigns` (
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
`client_id` int(11) NOT NULL DEFAULT 0,
|
`client_id` int(11) NOT NULL DEFAULT 0,
|
||||||
`campaign_id` bigint(20) NOT NULL DEFAULT 0,
|
`campaign_id` bigint(20) NOT NULL DEFAULT 0,
|
||||||
`campaign_name` varchar(255) NOT NULL DEFAULT '0',
|
`campaign_name` varchar(255) NOT NULL DEFAULT '0',
|
||||||
|
`advertising_channel_type` varchar(40) DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `client_id` (`client_id`),
|
KEY `client_id` (`client_id`),
|
||||||
CONSTRAINT `FK__clients` FOREIGN KEY (`client_id`) REFERENCES `clients` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
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;
|
) ENGINE=InnoDB AUTO_INCREMENT=56 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
|
-- Zrzut struktury tabela host700513_adspro.campaigns_history
|
||||||
CREATE TABLE IF NOT EXISTS `campaigns_history` (
|
CREATE TABLE IF NOT EXISTS `campaigns_history` (
|
||||||
@@ -54,235 +34,7 @@ CREATE TABLE IF NOT EXISTS `campaigns_history` (
|
|||||||
PRIMARY KEY (`id`) USING BTREE,
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
KEY `offer_id` (`campaign_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
|
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;
|
) ENGINE=InnoDB AUTO_INCREMENT=381 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_merchant_account_id` varchar(32) 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` (
|
CREATE TABLE IF NOT EXISTS `campaign_ad_groups` (
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
@@ -304,67 +56,122 @@ CREATE TABLE IF NOT EXISTS `campaign_ad_groups` (
|
|||||||
`date_sync` date DEFAULT NULL,
|
`date_sync` date DEFAULT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `uk_campaign_ad_groups_campaign_ad_group` (`campaign_id`,`ad_group_id`),
|
UNIQUE KEY `uk_campaign_ad_groups_campaign_ad_group` (`campaign_id`,`ad_group_id`),
|
||||||
KEY `idx_campaign_ad_groups_campaign_id` (`campaign_id`)
|
KEY `idx_campaign_ad_groups_campaign_id` (`campaign_id`),
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
CONSTRAINT `FK_campaign_ad_groups_campaigns` FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=125 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `campaign_search_terms` (
|
CREATE TABLE IF NOT EXISTS `campaign_alerts` (
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
`campaign_id` int(11) NOT NULL,
|
`client_id` int(11) NOT NULL,
|
||||||
`ad_group_id` int(11) NOT NULL,
|
`campaign_id` int(11) DEFAULT NULL,
|
||||||
`search_term` varchar(255) NOT NULL,
|
`campaign_external_id` bigint(20) DEFAULT 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_keywords` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`campaign_id` int(11) NOT NULL,
|
|
||||||
`ad_group_id` int(11) NOT NULL,
|
|
||||||
`keyword_text` varchar(255) NOT NULL,
|
|
||||||
`match_type` varchar(40) DEFAULT 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_keywords` (`campaign_id`,`ad_group_id`,`keyword_text`(191),`match_type`),
|
|
||||||
KEY `idx_campaign_keywords_campaign_id` (`campaign_id`),
|
|
||||||
KEY `idx_campaign_keywords_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,
|
`ad_group_id` int(11) DEFAULT NULL,
|
||||||
`scope` varchar(20) NOT NULL DEFAULT 'campaign',
|
`ad_group_external_id` bigint(20) DEFAULT NULL,
|
||||||
`keyword_text` varchar(255) NOT NULL,
|
`product_id` int(11) DEFAULT NULL,
|
||||||
`match_type` varchar(40) DEFAULT NULL,
|
`alert_type` varchar(120) NOT NULL,
|
||||||
`date_sync` date DEFAULT NULL,
|
`message` text NOT NULL,
|
||||||
|
`meta_json` text DEFAULT NULL,
|
||||||
|
`date_detected` date NOT NULL,
|
||||||
|
`date_add` datetime NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`unseen` tinyint(1) NOT NULL DEFAULT 1,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_campaign_negative_keywords_campaign_id` (`campaign_id`),
|
UNIQUE KEY `uniq_alert_daily` (`client_id`,`campaign_external_id`,`ad_group_external_id`,`alert_type`,`date_detected`),
|
||||||
KEY `idx_campaign_negative_keywords_ad_group_id` (`ad_group_id`)
|
KEY `idx_alert_date` (`date_detected`),
|
||||||
|
KEY `idx_alert_client` (`client_id`),
|
||||||
|
KEY `idx_alert_campaign` (`campaign_id`),
|
||||||
|
KEY `idx_alert_ad_group` (`ad_group_id`),
|
||||||
|
KEY `idx_alert_unseen` (`unseen`),
|
||||||
|
KEY `idx_alert_product` (`product_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||||
|
|
||||||
|
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,
|
||||||
|
`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,
|
||||||
|
`merchant_url_not_found` tinyint(1) NOT NULL DEFAULT 0,
|
||||||
|
`merchant_url_last_check` datetime 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=8482 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `products_history` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`product_id` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`campaign_id` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`ad_group_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,
|
||||||
|
UNIQUE KEY `uk_products_history_scope_day` (`product_id`,`campaign_id`,`ad_group_id`,`date_add`),
|
||||||
|
KEY `product_id` (`product_id`) USING BTREE,
|
||||||
|
KEY `idx_products_history_campaign_id` (`campaign_id`),
|
||||||
|
KEY `idx_products_history_ad_group_id` (`ad_group_id`),
|
||||||
|
CONSTRAINT `FK_products_history_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=37033 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `products_history_30` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`product_id` int(11) NOT NULL,
|
||||||
|
`campaign_id` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`ad_group_id` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`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`),
|
||||||
|
UNIQUE KEY `uk_products_history_30_scope_day` (`product_id`,`campaign_id`,`ad_group_id`,`date_add`),
|
||||||
|
KEY `product_id` (`product_id`) USING BTREE,
|
||||||
|
KEY `idx_products_history_30_campaign_id` (`campaign_id`),
|
||||||
|
KEY `idx_products_history_30_ad_group_id` (`ad_group_id`),
|
||||||
|
CONSTRAINT `FK_products_history_30_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `products_aggregate` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`product_id` int(11) NOT NULL,
|
||||||
|
`campaign_id` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`ad_group_id` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`impressions_30` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`clicks_30` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`ctr_30` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||||
|
`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,
|
||||||
|
`ctr_all_time` decimal(20,6) NOT NULL DEFAULT 0.000000,
|
||||||
|
`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 NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_products_aggregate_scope` (`product_id`,`campaign_id`,`ad_group_id`),
|
||||||
|
KEY `idx_products_aggregate_campaign_id` (`campaign_id`),
|
||||||
|
KEY `idx_products_aggregate_ad_group_id` (`ad_group_id`),
|
||||||
|
KEY `idx_products_aggregate_date_sync` (`date_sync`),
|
||||||
|
CONSTRAINT `FK_products_aggregate_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|||||||
426
docs/memory.md
426
docs/memory.md
@@ -1,55 +1,397 @@
|
|||||||
# adsPRO - Pamiec projektu
|
# 2026-02-20 - Obsluga statusu ACTIVE dla klientow
|
||||||
|
|
||||||
Ten plik sluzy jako trwala pamiec dla Claude Code. Zapisuj tu wzorce, decyzje i ustalenia potwierdzone w trakcie pracy nad projektem.
|
## Zmienione pliki
|
||||||
|
|
||||||
## Architektura
|
- `autoload/controls/class.Clients.php`
|
||||||
|
- `save()` zapisuje teraz pole `active` (domyslnie `1`, gdy brak wartosci z formularza).
|
||||||
|
- Dodana nowa akcja `set_active()` pod endpoint `/clients/set_active` do szybkiej zmiany statusu klienta AJAX-em.
|
||||||
|
- `force_sync()` ma dodatkowa walidacje:
|
||||||
|
- nie pozwala kolejkowac synchronizacji dla klienta nieaktywnego (`active != 1`),
|
||||||
|
- nadal blokuje klienta usunietego (`deleted = 1`) i klienta bez wymaganych ID.
|
||||||
|
- Kompatybilnosc schematu `clients` bez kolumny `deleted`:
|
||||||
|
- helpery `clients_has_deleted_column()` i `sql_clients_not_deleted()`,
|
||||||
|
- `force_sync()` i `sync_status()` nie wywalaja sie, gdy w bazie nie ma kolumny `deleted`.
|
||||||
|
|
||||||
- Custom MVC: Controllers (`\controls`) -> Factories (`\factory`) -> Medoo ORM (`$mdb`)
|
- `templates/clients/main_view.php`
|
||||||
- Autoload PSR-0: `\controls\Foo` -> `autoload/controls/class.Foo.php`
|
- Tabela klientow ma nowa kolumne `Status` (Aktywny/Nieaktywny).
|
||||||
- Routing w `index.php`: URL `/module/action/` -> `\controls\Module::action()`
|
- Wiersz klienta trzyma `data-active` do obslugi UI i synchronizacji.
|
||||||
- Szablony w `templates/`, zmienne przez `$this->varName`
|
- Dodany przycisk toggle (ikona `fa-toggle-on/off`) do natychmiastowej aktywacji/dezaktywacji.
|
||||||
- Serwisy API: `\services\GoogleAdsApi`, `\services\ClaudeApi`, `\services\OpenAiApi`
|
- Przyciski synchronizacji (kampanie/produkty/merchant) sa blokowane (`disabled`) dla nieaktywnego klienta i odblokowywane po aktywacji.
|
||||||
|
- Formularz Dodaj/Edytuj klienta ma nowe pole `Status klienta` (`active`).
|
||||||
|
- JS:
|
||||||
|
- `toggleClientActive()` wysyla POST na `/clients/set_active`,
|
||||||
|
- `updateClientStatusUI()` odswieza status i stan komorki Sync bez przeladowania strony,
|
||||||
|
- `loadSyncStatus()` pomija paski postepu dla nieaktywnych klientow i pokazuje `nieaktywny`.
|
||||||
|
|
||||||
## Styl kodu
|
## Gdzie to jest wykorzystywane
|
||||||
|
|
||||||
- Spacje w nawiasach: `if ( $x )`, `function( $a, $b )`
|
- Zarzadzanie statusem klienta:
|
||||||
- Klamry w nowej linii
|
- UI listy i formularza: `templates/clients/main_view.php`
|
||||||
- Wszystkie metody kontrolerow i fabryk: `static public function`
|
- Backend zapisu i toggle: `autoload/controls/class.Clients.php`
|
||||||
- Endpointy JSON: `echo json_encode([...]); exit;`
|
- Ograniczenie recznego wymuszenia synchronizacji do klientow aktywnych:
|
||||||
- Commity po polsku z prefixem: `feat:`, `fix:`, `update:`
|
- `autoload/controls/class.Clients.php` (`force_sync()`)
|
||||||
|
|
||||||
## Frontend
|
# 2026-02-20 - CRON kampanii (nowy przebieg, stare jako archiwum)
|
||||||
|
|
||||||
- jQuery 3.6, DataTables 2.1, Bootstrap 4, Select2 4.1, Highcharts
|
## Zmienione pliki
|
||||||
- jquery-confirm do modali/dialogow
|
|
||||||
- Font Awesome 6.5 do ikon
|
|
||||||
- SASS: `layout/style.scss` -> auto-kompilacja przez Live Sass Compiler
|
|
||||||
|
|
||||||
## Deployment
|
- `autoload/controls/class.Cron.php`
|
||||||
|
- Dodany nowy `cron_campaigns()` jako glowny endpoint pod nowy przeplyw.
|
||||||
|
- Stary kod zostal zachowany jako archiwum: `cron_campaigns_archive()`.
|
||||||
|
- Nowy przebieg:
|
||||||
|
- bierze tylko aktywnych klientow (`active = 1`) z Google Ads Customer ID,
|
||||||
|
- liczy okno dat na podstawie `google_ads_conversion_window_days` z `config.php` (z fallbackiem),
|
||||||
|
- konczy okno na `przedwczoraj` (bez pobierania danych dzisiejszych),
|
||||||
|
- przechodzi po datach dzien po dniu (rosnaco),
|
||||||
|
- zapisuje/aktualizuje kampanie do `campaigns`,
|
||||||
|
- zapisuje/aktualizuje historie dzienne do `campaigns_history` (upsert po `campaign_id + date_add`),
|
||||||
|
- zapisuje grupy reklam / groupy PMAX do `campaign_ad_groups`.
|
||||||
|
- po zakonczeniu kampanii + ad groups dla klienta, dla calego okna dat pobiera search terms dzienne do `campaign_search_terms_history`,
|
||||||
|
- po pobraniu historii search terms wykonuje agregacje do `campaign_search_terms` (zanim przejdzie do kolejnego klienta).
|
||||||
|
- Dodany krok syncu fraz dodanych i wykluczonych:
|
||||||
|
- tabele docelowe: `campaign_keywords` i `campaign_negative_keywords`,
|
||||||
|
- uruchamiany raz na cykl klienta (po ostatnim dniu okna), nie x razy dla kazdego dnia.
|
||||||
|
- Kampanie produktowe / PMAX:
|
||||||
|
- nie maja fraz dodanych, wiec w `campaign_keywords` moga miec 0 rekordow,
|
||||||
|
- frazy wykluczone sa dalej synchronizowane do `campaign_negative_keywords`.
|
||||||
|
|
||||||
- FTP auto-upload przez VS Code FTP-Kr
|
# 2026-02-20 - Produkty: przygotowanie schematu bazy
|
||||||
- Brak kroku budowania - pliki laduja bezposrednio na serwer
|
|
||||||
- Migracje: `php install.php` (idempotentne, sledzenie w `schema_migrations`)
|
|
||||||
|
|
||||||
## Decyzje projektowe
|
## Zmienione pliki
|
||||||
|
|
||||||
- Frazy wyszukiwane dodane do wykluczonych oznaczane czerwonym kolorem (klasa CSS `term-is-negative`)
|
- `migrations/016_products_model_unification.sql`
|
||||||
- Negatywne slowa kluczowe dodawane przez Google Ads API i zapisywane lokalnie w `campaign_negative_keywords`
|
- Dodane kolumny produktowe bezposrednio do `products`:
|
||||||
- Klucze API przechowywane w tabeli `settings` (key-value)
|
- `custom_label_4`, `custom_label_3`, `title`, `description`, `google_product_category`, `product_url`.
|
||||||
- Frazy z Google Ads Keyword Planner dla URL produktu sa cachowane w `products_keyword_planner_terms` i ponownie uzywane przy generowaniu tytulu AI
|
- Backfill danych z `products_data` -> `products` (tylko gdy pole w `products` jest puste).
|
||||||
- Zmiany produktowe (`title`, `description`, `google_product_category`, `custom_label_4`) sa synchronizowane bezposrednio do Merchant API i logowane per pole w `products_merchant_sync_log`
|
- Dodana nowa tabela agregacyjna `products_aggregate`:
|
||||||
- CRON dziala w trybie **klient po kliencie** (client-first): konczy WSZYSTKIE daty jednego klienta, potem przechodzi do nastepnego. Dzieki temu paski postepu na `/clients` roznia sie miedzy klientami.
|
- scope: `product_id + campaign_id + ad_group_id` (unikalne),
|
||||||
- `cron_products` iteruje po datach per klient (`dates_per_run` z parametru `clients_per_run`), domyslnie `10` (max `100`); faza `aggregate_30` wywoluje `rebuild_products_temp` RAZ per klient
|
- metryki `*_30` i `*_all_time`,
|
||||||
- `cron_campaigns` iteruje po datach per klient (`dates_per_run` z parametru `clients_per_run`), domyslnie `2` (max `20`)
|
- `date_sync` (kiedy agregat byl przeliczony).
|
||||||
- Helpery: `get_active_client($pipeline)` -> pierwszy klient z niezakonczona praca; `get_pending_dates_for_client()` -> daty do przetworzenia; `determine_client_products_phase()` -> faza per klient
|
|
||||||
- Stan CRON przechowywany w tabeli `cron_sync_status` (wiersz = klient + pipeline + data + phase), zamiast JSON w `settings` (migracja 012)
|
|
||||||
- Fazy produktow w `cron_sync_status`: pending -> fetch -> aggregate_30 -> done; kampanie: pending -> done
|
|
||||||
- Force sync klienta = DELETE z `cron_sync_status` (wiersze odtwarzane przez `ensure_sync_rows` w nastepnym cyklu CRON)
|
|
||||||
- Nowy klient/usuniety klient obslugiwany naturalnie: `ensure_sync_rows` dodaje brakujace, JOIN z `clients` pomija usunietych
|
|
||||||
- `cleanup_old_sync_rows(30)` czysci zakonczone wiersze starsze niz 30 dni i wiersze usunietych klientow
|
|
||||||
|
|
||||||
## Preferencje uzytkownika
|
- `docs/database.sql`
|
||||||
|
- Zaktualizowana definicja `products` o nowe kolumny danych produktu.
|
||||||
|
- Dodana definicja tabeli `products_aggregate`.
|
||||||
|
|
||||||
- Komunikacja po polsku
|
## Ustalenie projektowe
|
||||||
- Zwiezle commity po polsku
|
|
||||||
- Git push tylko na wyrazna prosbe
|
- `products` staje sie glowna tabela danych produktu.
|
||||||
|
- `products_data` zostaje tymczasowo dla kompatybilnosci starego kodu; dane sa migrowane do `products`.
|
||||||
|
- Agregaty dla widokow `/products` powinny docelowo byc czytane z `products_aggregate` zamiast liczenia w locie.
|
||||||
|
|
||||||
|
# 2026-02-20 - Produkty: przepiecie na `products` + agregaty
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `autoload/factory/class.Products.php`
|
||||||
|
- `get_product_data()`:
|
||||||
|
- najpierw czyta pola produktowe z `products` (`custom_label_4`, `custom_label_3`, `title`, `description`, `google_product_category`, `product_url`),
|
||||||
|
- fallback do `products_data` dla kompatybilnosci.
|
||||||
|
- `set_product_data()`:
|
||||||
|
- zapisuje pole glownie do `products`,
|
||||||
|
- rownolegle mirroruje zapis do `products_data` (kompatybilnosc starego kodu).
|
||||||
|
|
||||||
|
- `autoload/controls/class.Cron.php`
|
||||||
|
- `sync_products_fetch_for_client()`:
|
||||||
|
- import produktow zapisuje dane produktowe bezposrednio do `products` (w tym `title`, `product_url`),
|
||||||
|
- usuniete poleganie na `products_data` podczas samego fetchu.
|
||||||
|
- `aggregate_products_history_30_for_client()`:
|
||||||
|
- po przeliczeniu `products_history_30` odpala przebudowe agregatow `products_aggregate` dla klienta i dnia.
|
||||||
|
- Dodana metoda `rebuild_products_aggregate_for_client( $client_id, $date_sync )`:
|
||||||
|
- liczy metryki `*_30` i `*_all_time` z `products_history`,
|
||||||
|
- zapisuje scope (`product_id + campaign_id + ad_group_id`) do `products_aggregate`.
|
||||||
|
- `rebuild_products_temp_for_client()`:
|
||||||
|
- przestawione z liczenia bezposrednio po `products_history` na odczyt z `products_aggregate`,
|
||||||
|
- zmniejsza liczenie "w locie" dla widoku `/products`.
|
||||||
|
- `cron_product_history_30_save()`:
|
||||||
|
- `products_history_30` przechowuje teraz srednie dzienne wartosci z okna do 30 dni (zamiast sumy okna),
|
||||||
|
- nadal zapisuje `roas_all_time` dla danego dnia.
|
||||||
|
- `generate_custom_feed_for_client()`:
|
||||||
|
- zrodlo danych produktowych przepiete na `products` (bez wymaganego `INNER JOIN products_data`).
|
||||||
|
- diagnostyka i pobieranie brakujacych URL (`cron_products_urls`):
|
||||||
|
- logika "ma URL / brak URL" bierze pod uwage `products.product_url` z fallbackiem do `products_data`.
|
||||||
|
|
||||||
|
## Gdzie to jest wykorzystywane
|
||||||
|
|
||||||
|
- Pipeline produktowy:
|
||||||
|
- `/cron/cron_products`
|
||||||
|
- etap `fetch` -> `products_history`,
|
||||||
|
- etap agregacji -> `products_history_30` + `products_aggregate`,
|
||||||
|
- etap finalny -> `products_temp` budowane z `products_aggregate`.
|
||||||
|
- Widok tabeli produktow `/products`:
|
||||||
|
- dane nadal czytane z `products_temp`, ale `products_temp` jest teraz zasilane agregatami z `products_aggregate`.
|
||||||
|
- Dodany helper `sync_campaigns_snapshot_for_client()` dla nowego przebiegu kampanii.
|
||||||
|
- Dodany helper `sync_campaign_terms_backfill_for_client()` dla kroku fraz (history + agregacja).
|
||||||
|
- Tryb wykonania nowego pipeline kampanii: 1 dzien = 1 wywolanie CRON.
|
||||||
|
- Na jednym wywolaniu: kampanie + ad groups + search terms history + agregacja search terms dla jednego dnia.
|
||||||
|
- Kolejne wywolanie przechodzi do kolejnego dnia dla tego samego klienta.
|
||||||
|
- Tryb debug dla nowego CRON:
|
||||||
|
- `?debug=true` zwraca czytelny HTML (podsumowanie + pelny payload),
|
||||||
|
- bez debug zwracany jest standardowy JSON.
|
||||||
|
- Dodany helper `cleanup_pipeline_rows_outside_window()` aby pipeline kampanii trzymal tylko aktualne okno dat.
|
||||||
|
- Filtry klientow w nowym CRON kampanii sa odporne na stare dane (`NULL`): `COALESCE(active,0)`, `COALESCE(deleted,0)`, `TRIM(COALESCE(google_ads_customer_id,''))`.
|
||||||
|
- Dodana kompatybilnosc schematu `clients` bez kolumny `deleted`:
|
||||||
|
- helpery: `clients_has_column()`, `sql_clients_not_deleted()`, `sql_clients_deleted()`,
|
||||||
|
- nowy pipeline kampanii (`cron_campaigns`/`cron_universal`) nie wywala sie na bazie bez `deleted`.
|
||||||
|
- `get_conversion_window_days( $prefer_config = false )` uwzglednia teraz konfiguracje z `config.php`.
|
||||||
|
- `sync_campaign_ad_groups_for_client()` dostal parametr `as_of_date`.
|
||||||
|
|
||||||
|
- `autoload/services/class.GoogleAdsApi.php`
|
||||||
|
- `get_ad_groups_30_days()` wspiera teraz parametr `as_of_date` i zakres dat `[as_of_date-29, as_of_date]`.
|
||||||
|
- `get_ad_groups_all_time()` wspiera teraz parametr `as_of_date` (filtr `segments.date <= as_of_date` z fallbackiem).
|
||||||
|
|
||||||
|
## Gdzie to jest wykorzystywane
|
||||||
|
|
||||||
|
- Głowny CRON kampanii: `/cron/cron_campaigns` -> `\controls\Cron::cron_campaigns()`.
|
||||||
|
- Uniwersalny CRON pipeline (zalecany endpoint): `/cron/cron_universal` -> `\controls\Cron::cron_universal()` (aktualnie deleguje do kroku kampanii).
|
||||||
|
- Archiwalny CRON kampanii (stara logika): `/cron/cron_campaigns_archive`.
|
||||||
|
- Dane do wykresow/tabel kampanii pozostaja pobierane z `campaigns_history`.
|
||||||
|
|
||||||
|
# 2026-02-20 - CRON uniwersalny jako glowny endpoint (1 dzien na wywolanie)
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `autoload/controls/class.Cron.php`
|
||||||
|
- `cron_universal()` nie deleguje juz do `cron_campaigns()`.
|
||||||
|
- W jednym wywolaniu realizuje sekwencje:
|
||||||
|
- `kampanie` (snapshot + ad groups + search terms + agregacja),
|
||||||
|
- `produkty` (fetch + `products_history_30` + `products_aggregate` + `products_temp`).
|
||||||
|
- Tryb pracy pozostaje: `1 wywolanie = 1 klient + 1 dzien`.
|
||||||
|
- Status dnia jest zapisywany do `cron_sync_status` dla obu pipeline:
|
||||||
|
- `campaigns`,
|
||||||
|
- `products`.
|
||||||
|
- Gdy krok kampanii zwroci blad, krok produktow dla tego dnia jest pomijany (`products_sync_skipped_reason=campaigns_failed`).
|
||||||
|
|
||||||
|
## Gdzie to jest wykorzystywane
|
||||||
|
|
||||||
|
- Docelowy adres CRON:
|
||||||
|
- `/cron/cron_universal?debug=true`
|
||||||
|
- Stare endpointy (`/cron/cron_campaigns`, `/cron/cron_products`) pozostaja w kodzie, ale nie sa docelowa sciezka wykonywania.
|
||||||
|
|
||||||
|
# 2026-02-20 - Poprawka niezaleznosci pipeline w `cron_universal`
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
- `campaigns` mialo juz 100% (`done`) i `cron_universal` konczyl wykonanie, mimo ze `products` mial jeszcze zalegle daty.
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `autoload/controls/class.Cron.php`
|
||||||
|
- `cron_universal()` wybiera teraz aktywnego klienta niezaleznie dla obu pipeline:
|
||||||
|
- `campaigns`,
|
||||||
|
- `products`.
|
||||||
|
- Zakonczenie "wszyscy przetworzeni" następuje dopiero, gdy **oba** pipeline nie maja juz aktywnych pozycji.
|
||||||
|
- Dodane osobne liczenie pozostalych dat:
|
||||||
|
- `campaigns_remaining_dates`,
|
||||||
|
- `products_remaining_dates`.
|
||||||
|
- Statusy `done/pending` sa zapisywane osobno dla kazdego pipeline; produkty nie sa juz blokowane przez sam fakt, ze kampanie sa skonczone globalnie.
|
||||||
|
- Ujednolicenie trybu `client_id`:
|
||||||
|
- kampanie i produkty wykonują sie niezaleznie (w tym samym wywolaniu), a bledy sa laczone tylko w odpowiedzi.
|
||||||
|
|
||||||
|
# 2026-02-20 - Usuniecie `products_data`
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `migrations/017_drop_products_data.sql`
|
||||||
|
- Dodana migracja usuwajaca tabele `products_data`.
|
||||||
|
|
||||||
|
- `autoload/factory/class.Products.php`
|
||||||
|
- `get_product_data()` czyta dane tylko z `products`.
|
||||||
|
- `set_product_data()` zapisuje dane tylko do `products`.
|
||||||
|
|
||||||
|
- `autoload/controls/class.Cron.php`
|
||||||
|
- diagnostyka URL i wybieranie produktow bez URL opiera sie juz tylko o `products.product_url`.
|
||||||
|
|
||||||
|
- `docs/database.sql`
|
||||||
|
- usunieta definicja tabeli `products_data`.
|
||||||
|
|
||||||
|
- `migrations/demo_data.sql`
|
||||||
|
- usuniete operacje `INSERT/DELETE` na `products_data`,
|
||||||
|
- etykiety demo (`custom_label_4`) sa ustawiane bezposrednio w `products`.
|
||||||
|
|
||||||
|
## Gdzie to jest wykorzystywane
|
||||||
|
|
||||||
|
- Dane produktowe (`title`, `description`, `google_product_category`, `custom_label_3`, `custom_label_4`, `product_url`) sa trzymane tylko w `products`.
|
||||||
|
|
||||||
|
# 2026-02-20 - Ostatni krok `cron_universal`: URL z Merchant + alerty brakow
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `autoload/controls/class.Cron.php`
|
||||||
|
- Dodany helper `sync_products_urls_and_alerts_for_client()`.
|
||||||
|
- Na koncu przebiegu `cron_universal` (zarowno tryb automatyczny, jak i `client_id`) wykonywany jest krok:
|
||||||
|
- pobranie URL produktow z Google Merchant Center dla produktow bez URL,
|
||||||
|
- zapis URL do `products.product_url`.
|
||||||
|
- Gdy `offer_id` nie istnieje w Merchant Center, tworzony/aktualizowany jest alert w `campaign_alerts`:
|
||||||
|
- `alert_type = products_missing_in_merchant_center`,
|
||||||
|
- scope techniczny: `campaign_external_id = 0`, `ad_group_external_id = 0`,
|
||||||
|
- `meta_json` zawiera m.in. listy `missing_offer_ids` i `missing_product_ids`.
|
||||||
|
- Gdy w danym dniu brak brakujacych produktow, dzienny alert tego typu jest czyszczony.
|
||||||
|
- Do odpowiedzi cron dodane pola diagnostyczne:
|
||||||
|
- `merchant_urls_checked`,
|
||||||
|
- `merchant_urls_updated`,
|
||||||
|
- `merchant_missing_in_mc_count`,
|
||||||
|
- `merchant_missing_offer_ids`.
|
||||||
|
- `cron_universal` ma dodatkowy fallback niezalezny od pipeline `campaigns/products`:
|
||||||
|
- gdy oba pipeline sa zakonczone, ale sa jeszcze produkty bez URL, uruchamia sam krok Merchant URL + alerty (`merchant_only=1`),
|
||||||
|
- dopiero brak takich produktow daje komunikat "Wszyscy aktywni klienci zostali przetworzeni...".
|
||||||
|
- Krok Merchant URL nie jest wykonywany dla kazdego dnia okna; dziala jako osobny etap po zakonczeniu `campaigns/products`.
|
||||||
|
- Do zapytan do GMC trafiaja tylko produkty z `products.product_url IS NULL` i `merchant_url_not_found = 0`.
|
||||||
|
- Na jedno wywolanie wykonywana jest jedna paczka sprawdzen (limit z `config.php`: `cron_products_urls_limit_per_client`, ustawiony na `100`).
|
||||||
|
- Produkty, ktorych GMC nie zwraca (brak URL), sa oznaczane:
|
||||||
|
- `products.merchant_url_not_found = 1`,
|
||||||
|
- `products.merchant_url_last_check = NOW()`,
|
||||||
|
- dzieki temu nie sa wysylane ponownie w nieskonczonosc.
|
||||||
|
- Alert `products_missing_in_merchant_center` jest liczony na podstawie calej aktualnej puli `merchant_url_not_found = 1` (nie tylko bieżącej paczki), wiec nie znika przy `checked_products = 0`.
|
||||||
|
- Alerty sa per produkt (1 alert = 1 produkt):
|
||||||
|
- dla kazdego produktu bez URL i z `merchant_url_not_found = 1` tworzony jest osobny wpis w `campaign_alerts`,
|
||||||
|
- tresc alertu zawiera nazwe produktu (fallback: `name`, dalej `offer_id`) i `offer_id`,
|
||||||
|
- technicznie: `campaign_external_id = products.id`, co stabilizuje unikalnosc wpisu.
|
||||||
|
|
||||||
|
- `migrations/018_products_merchant_url_flags.sql`
|
||||||
|
- Dodane kolumny w `products`:
|
||||||
|
- `merchant_url_not_found` (TINYINT, domyslnie 0),
|
||||||
|
- `merchant_url_last_check` (DATETIME).
|
||||||
|
- Normalizacja: puste/sztuczne `product_url` (`'', '0', '-', 'null'`) ustawiane na `NULL`.
|
||||||
|
|
||||||
|
- `autoload/factory/class.Products.php`
|
||||||
|
- Przy zapisie `product_url`:
|
||||||
|
- ustawiany jest `merchant_url_last_check`,
|
||||||
|
- dla poprawnego URL resetowane jest `merchant_url_not_found = 0`.
|
||||||
|
|
||||||
|
# 2026-02-20 - Alerty na stronie `/products` dla klient + kampania
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `autoload/factory/class.Products.php`
|
||||||
|
- `get_scope_alerts()` nie wymaga juz wybranej grupy reklam:
|
||||||
|
- minimalny scope: `client_id + campaign_id`,
|
||||||
|
- filtr `ad_group_id` jest stosowany tylko opcjonalnie (gdy grupa jest wybrana).
|
||||||
|
|
||||||
|
- `templates/products/main_view.php`
|
||||||
|
- `load_scope_alerts()` pobiera alerty juz dla kombinacji `klient + kampania`.
|
||||||
|
- Sekcja alertow ma zaktualizowany opis: kampania + opcjonalna grupa reklam.
|
||||||
|
|
||||||
|
## Gdzie to jest wykorzystywane
|
||||||
|
|
||||||
|
- `/products`
|
||||||
|
- Panel alertow pod filtrami pokazuje alerty:
|
||||||
|
- dla calej kampanii (gdy grupa reklam nie jest wybrana),
|
||||||
|
- lub zawezone do konkretnej grupy (gdy grupa reklam jest wybrana).
|
||||||
|
|
||||||
|
# 2026-02-20 - Etykietowanie alertow Merchant (bez falszywej kampanii)
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `autoload/controls/class.Cron.php`
|
||||||
|
- Dla alertu `products_missing_in_merchant_center` nie jest juz zapisywany `product_id` w `campaign_external_id`.
|
||||||
|
- Pola scope kampanii/grupy sa zapisywane jako `0` (alert produktowy, bez przypisania do kampanii).
|
||||||
|
|
||||||
|
- `templates/campaign_alerts/main_view.php`
|
||||||
|
- Dla alertu `products_missing_in_merchant_center` tabela alertow pokazuje:
|
||||||
|
- Kampania: `Produkt (Merchant Center)`,
|
||||||
|
- Grupa reklam: `---`.
|
||||||
|
- Dla pozostalych alertow fallback `Kampania #...` / `Grupa reklam #...` dziala tylko dla dodatnich external_id; dla `0` pokazuje neutralne etykiety.
|
||||||
|
|
||||||
|
# 2026-02-20 - Powiazanie `campaign_alerts` z `products`
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `migrations/019_campaign_alerts_product_id.sql`
|
||||||
|
- Dodana kolumna `campaign_alerts.product_id` (NULL) oraz indeks `idx_alert_product`.
|
||||||
|
|
||||||
|
- `autoload/controls/class.Cron.php`
|
||||||
|
- Alerty `products_missing_in_merchant_center` zapisuja `product_id` w tabeli `campaign_alerts`.
|
||||||
|
- Dla zachowania unikalnosci dziennej per produkt, techniczny `campaign_external_id` pozostaje rowny `product_id`.
|
||||||
|
|
||||||
|
- `autoload/factory/class.CampaignAlerts.php`
|
||||||
|
- `get_alerts()` zwraca teraz rowniez pole `product_id`.
|
||||||
|
|
||||||
|
- `docs/database.sql`
|
||||||
|
- Dodana aktualna definicja tabeli `campaign_alerts` z kolumna `product_id`.
|
||||||
|
|
||||||
|
# 2026-02-20 - CRON produktow: `title` nie jest uzupelniany automatycznie
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `autoload/controls/class.Cron.php`
|
||||||
|
- W syncu produktow do tabeli `products` CRON nie zapisuje juz pola `title`.
|
||||||
|
- Dla nowych produktow CRON zapisuje tylko `name` (bez `title`).
|
||||||
|
- Dla istniejacych produktow usunieto automatyczne uzupelnianie pustego `title`.
|
||||||
|
|
||||||
|
## Gdzie to jest wykorzystywane
|
||||||
|
|
||||||
|
- `/cron/cron_universal`
|
||||||
|
- automatyczny import produktow nie nadpisuje ani nie uzupelnia `products.title`,
|
||||||
|
- `title` pozostaje polem do recznej edycji i wysylki do GMC.
|
||||||
|
|
||||||
|
# 2026-02-20 - Lista produktow z 0 wyswietlen (30 dni) na `/products`
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `autoload/factory/class.Products.php`
|
||||||
|
- Dodana metoda `get_products_without_impressions_30( $client_id, $campaign_id, $limit )`.
|
||||||
|
- Zwraca produkty z wybranej kampanii, ktore maja sume `impressions_30 = 0` na podstawie `products_aggregate`.
|
||||||
|
- Dodatkowy filtr `ad_group_id` (opcjonalny), aby lista byla zgodna z aktualnym filtrem grupy reklam na widoku.
|
||||||
|
|
||||||
|
- `autoload/controls/class.Products.php`
|
||||||
|
- Dodany endpoint `get_products_without_impressions_30()`.
|
||||||
|
- Zwraca JSON: `status`, `products[]`, `count` i przyjmuje opcjonalnie `ad_group_id`.
|
||||||
|
|
||||||
|
- `templates/products/main_view.php`
|
||||||
|
- Dodana sekcja nad tabela produktow:
|
||||||
|
- "Produkty do sprawdzenia (0 wyswietlen w ostatnich 30 dniach)".
|
||||||
|
- Sekcja pojawia sie dla wybranego `klient + kampania`.
|
||||||
|
- Lista odswieza sie przy zmianie klienta/kampanii/grupy oraz po zaladowaniu strony.
|
||||||
|
|
||||||
|
## Gdzie to jest wykorzystywane
|
||||||
|
|
||||||
|
- `/products`
|
||||||
|
- pomocnicza lista produktow potencjalnie nieistniejacych / wymagajacych weryfikacji (0 wyswietlen w 30 dni dla wybranej kampanii).
|
||||||
|
|
||||||
|
# 2026-02-20 - Ustawienia CRON: poprawka licznika klientow + usuniecie "Krok 1/Krok 2"
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `autoload/controls/class.Users.php`
|
||||||
|
- Licznik `Klienci z Google Ads ID` liczy teraz klientow z:
|
||||||
|
- `COALESCE(active, 0) = 1`,
|
||||||
|
- `TRIM(COALESCE(google_ads_customer_id, '')) <> ''`.
|
||||||
|
- Analogicznie poprawione filtry dla klientow Merchant i zapytan pomocniczych (wg `active`).
|
||||||
|
- Harmonogram krokow (`Krok 1`, `Krok 2`) w danych dashboardu CRON jest pusty.
|
||||||
|
|
||||||
|
- `templates/users/settings.php`
|
||||||
|
- Usunieta sekcja wizualna harmonogramu krokow CRON (`Krok 1` / `Krok 2`).
|
||||||
|
- Usunieta obsluga renderowania tej sekcji w JS odswiezajacym status CRON.
|
||||||
|
|
||||||
|
## Gdzie to jest wykorzystywane
|
||||||
|
|
||||||
|
- `/settings?settings_tab=cron`
|
||||||
|
- licznik klientow z Google Ads ID pokazuje poprawna wartosc na podstawie aktywnych klientow (`active = 1`),
|
||||||
|
- brak sekcji "Krok 1 / Krok 2".
|
||||||
|
|
||||||
|
# 2026-02-20 - `/products` czyta bezposrednio z `products_aggregate`
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `autoload/factory/class.Products.php`
|
||||||
|
- Zapytania dla listy produktow i licznikow zostaly przepiete z `products_temp` na `products_aggregate`:
|
||||||
|
- `get_products()`,
|
||||||
|
- `get_roas_bounds()`,
|
||||||
|
- `get_records_total_products()`,
|
||||||
|
- `get_product_full_context()`.
|
||||||
|
- Metryki all-time sa liczone z pol:
|
||||||
|
- `impressions_all_time`, `clicks_all_time`, `cost_all_time`, `conversions_all_time`, `conversion_value_all_time`.
|
||||||
|
- Metryki 30d sa czytane z:
|
||||||
|
- `impressions_30`, `clicks_30`.
|
||||||
|
|
||||||
|
## Gdzie to jest wykorzystywane
|
||||||
|
|
||||||
|
- `/products`
|
||||||
|
- tabela i liczniki nie zaleza juz od `products_temp`; biora dane bezposrednio z `products_aggregate`.
|
||||||
|
|
||||||
|
# 2026-02-20 - `custom_label_4` tylko z tabeli `products`
|
||||||
|
|
||||||
|
## Ustalenie
|
||||||
|
|
||||||
|
- Etykieta `custom_label_4` jest czytana i zapisywana z tabeli `products`.
|
||||||
|
- Agregaty (`products_aggregate`) nie sa zrodlem dla pola `custom_label_4`.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1856,6 +1856,44 @@ table {
|
|||||||
flex: 1 1 260px;
|
flex: 1 1 260px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.filter-group-ad-group {
|
||||||
|
.ad-group-filter-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#delete-products-ad-group {
|
||||||
|
min-width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 0;
|
||||||
|
background: #dc3545;
|
||||||
|
border: 1px solid #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: #bb2d3b;
|
||||||
|
border-color: #bb2d3b;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
cursor: default;
|
||||||
|
background: #dc3545;
|
||||||
|
border-color: #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.filter-group-roas {
|
&.filter-group-roas {
|
||||||
flex: 0 0 200px;
|
flex: 0 0 200px;
|
||||||
}
|
}
|
||||||
@@ -1866,6 +1904,73 @@ table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.products-scope-alerts {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
border: 1px solid #FECACA;
|
||||||
|
background: #FEF2F2;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
summary {
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #991B1B;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-scope-alerts-list {
|
||||||
|
border-top: 1px solid #FECACA;
|
||||||
|
background: #FFF;
|
||||||
|
max-height: 260px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-scope-alert-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-bottom: 1px solid #F1F5F9;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-scope-alert-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-scope-alert-type {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #EEF2FF;
|
||||||
|
color: #4338CA;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.products-scope-alert-message {
|
||||||
|
font-size: 13px;
|
||||||
|
color: $cTextDark;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.products-actions {
|
.products-actions {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
@@ -2458,6 +2563,37 @@ table#products {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cron-schedule-list {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cron-schedule-item {
|
||||||
|
border: 1px solid #DFE7F0;
|
||||||
|
background: #F4F8FD;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
color: $cTextDark;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
display: block;
|
||||||
|
color: #667788;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.cron-progress-item {
|
.cron-progress-item {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
|
|
||||||
@@ -2553,6 +2689,13 @@ table#products {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cron-url-plan {
|
||||||
|
display: block;
|
||||||
|
color: #6C7B8A;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
|
|||||||
165
migrations/016_products_model_unification.sql
Normal file
165
migrations/016_products_model_unification.sql
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
-- Migracja: unifikacja modelu danych produktow
|
||||||
|
-- Cel:
|
||||||
|
-- 1) przeniesienie danych produktowych do tabeli products (kolumny dotad trzymane glownie w products_data)
|
||||||
|
-- 2) dodanie tabeli agregatow products_aggregate (30 dni + all-time)
|
||||||
|
|
||||||
|
-- ===========================
|
||||||
|
-- products: nowe kolumny danych produktu
|
||||||
|
-- ===========================
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'products'
|
||||||
|
AND COLUMN_NAME = 'custom_label_4'
|
||||||
|
),
|
||||||
|
'DO 1',
|
||||||
|
'ALTER TABLE `products` ADD COLUMN `custom_label_4` VARCHAR(255) NULL DEFAULT NULL AFTER `min_roas`'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'products'
|
||||||
|
AND COLUMN_NAME = 'custom_label_3'
|
||||||
|
),
|
||||||
|
'DO 1',
|
||||||
|
'ALTER TABLE `products` ADD COLUMN `custom_label_3` VARCHAR(255) NULL DEFAULT NULL AFTER `custom_label_4`'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'products'
|
||||||
|
AND COLUMN_NAME = 'title'
|
||||||
|
),
|
||||||
|
'DO 1',
|
||||||
|
'ALTER TABLE `products` ADD COLUMN `title` VARCHAR(255) NULL DEFAULT NULL AFTER `custom_label_3`'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'products'
|
||||||
|
AND COLUMN_NAME = 'description'
|
||||||
|
),
|
||||||
|
'DO 1',
|
||||||
|
'ALTER TABLE `products` ADD COLUMN `description` TEXT NULL AFTER `title`'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'products'
|
||||||
|
AND COLUMN_NAME = 'google_product_category'
|
||||||
|
),
|
||||||
|
'DO 1',
|
||||||
|
'ALTER TABLE `products` ADD COLUMN `google_product_category` TEXT NULL AFTER `description`'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'products'
|
||||||
|
AND COLUMN_NAME = 'product_url'
|
||||||
|
),
|
||||||
|
'DO 1',
|
||||||
|
'ALTER TABLE `products` ADD COLUMN `product_url` VARCHAR(500) NULL DEFAULT NULL AFTER `google_product_category`'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- ===========================
|
||||||
|
-- Backfill products <- products_data
|
||||||
|
-- (zostawiamy products_data na razie dla kompatybilnosci)
|
||||||
|
-- ===========================
|
||||||
|
|
||||||
|
UPDATE `products` p
|
||||||
|
INNER JOIN `products_data` pd ON pd.product_id = p.id
|
||||||
|
SET
|
||||||
|
p.custom_label_4 = CASE
|
||||||
|
WHEN ( p.custom_label_4 IS NULL OR TRIM( p.custom_label_4 ) = '' ) AND pd.custom_label_4 IS NOT NULL AND TRIM( pd.custom_label_4 ) <> '' THEN pd.custom_label_4
|
||||||
|
ELSE p.custom_label_4
|
||||||
|
END,
|
||||||
|
p.custom_label_3 = CASE
|
||||||
|
WHEN ( p.custom_label_3 IS NULL OR TRIM( p.custom_label_3 ) = '' ) AND pd.custom_label_3 IS NOT NULL AND TRIM( pd.custom_label_3 ) <> '' THEN pd.custom_label_3
|
||||||
|
ELSE p.custom_label_3
|
||||||
|
END,
|
||||||
|
p.title = CASE
|
||||||
|
WHEN ( p.title IS NULL OR TRIM( p.title ) = '' ) AND pd.title IS NOT NULL AND TRIM( pd.title ) <> '' THEN pd.title
|
||||||
|
ELSE p.title
|
||||||
|
END,
|
||||||
|
p.description = CASE
|
||||||
|
WHEN ( p.description IS NULL OR TRIM( p.description ) = '' ) AND pd.description IS NOT NULL AND TRIM( pd.description ) <> '' THEN pd.description
|
||||||
|
ELSE p.description
|
||||||
|
END,
|
||||||
|
p.google_product_category = CASE
|
||||||
|
WHEN ( p.google_product_category IS NULL OR TRIM( p.google_product_category ) = '' ) AND pd.google_product_category IS NOT NULL AND TRIM( pd.google_product_category ) <> '' THEN pd.google_product_category
|
||||||
|
ELSE p.google_product_category
|
||||||
|
END,
|
||||||
|
p.product_url = CASE
|
||||||
|
WHEN ( p.product_url IS NULL OR TRIM( p.product_url ) = '' ) AND pd.product_url IS NOT NULL AND TRIM( pd.product_url ) <> '' THEN pd.product_url
|
||||||
|
ELSE p.product_url
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- ===========================
|
||||||
|
-- Tabela agregatow produktow
|
||||||
|
-- ===========================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `products_aggregate` (
|
||||||
|
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`product_id` INT(11) NOT NULL,
|
||||||
|
`campaign_id` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`ad_group_id` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`impressions_30` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`clicks_30` INT(11) NOT NULL DEFAULT 0,
|
||||||
|
`ctr_30` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||||
|
`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,
|
||||||
|
`ctr_all_time` DECIMAL(20,6) NOT NULL DEFAULT 0.000000,
|
||||||
|
`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 NOT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_products_aggregate_scope` (`product_id`,`campaign_id`,`ad_group_id`),
|
||||||
|
KEY `idx_products_aggregate_campaign_id` (`campaign_id`),
|
||||||
|
KEY `idx_products_aggregate_ad_group_id` (`ad_group_id`),
|
||||||
|
KEY `idx_products_aggregate_date_sync` (`date_sync`),
|
||||||
|
CONSTRAINT `FK_products_aggregate_products`
|
||||||
|
FOREIGN KEY (`product_id`) REFERENCES `products` (`id`)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
5
migrations/017_drop_products_data.sql
Normal file
5
migrations/017_drop_products_data.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
-- Migracja: usuniecie tabeli products_data
|
||||||
|
-- Dane produktowe sa juz przechowywane bezposrednio w tabeli products.
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `products_data`;
|
||||||
|
|
||||||
38
migrations/018_products_merchant_url_flags.sql
Normal file
38
migrations/018_products_merchant_url_flags.sql
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
-- Flagi kontroli pobierania URL z Merchant Center
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'products'
|
||||||
|
AND COLUMN_NAME = 'merchant_url_not_found'
|
||||||
|
),
|
||||||
|
'DO 1',
|
||||||
|
'ALTER TABLE `products` ADD COLUMN `merchant_url_not_found` TINYINT(1) NOT NULL DEFAULT 0 AFTER `product_url`'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'products'
|
||||||
|
AND COLUMN_NAME = 'merchant_url_last_check'
|
||||||
|
),
|
||||||
|
'DO 1',
|
||||||
|
'ALTER TABLE `products` ADD COLUMN `merchant_url_last_check` DATETIME NULL DEFAULT NULL AFTER `merchant_url_not_found`'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- Normalizacja: puste/sztuczne wartosci URL traktujemy jako brak URL.
|
||||||
|
UPDATE `products`
|
||||||
|
SET `product_url` = NULL
|
||||||
|
WHERE TRIM( COALESCE( `product_url`, '' ) ) = ''
|
||||||
|
OR LOWER( TRIM( `product_url` ) ) IN ( '0', '-', 'null' );
|
||||||
|
|
||||||
31
migrations/019_campaign_alerts_product_id.sql
Normal file
31
migrations/019_campaign_alerts_product_id.sql
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
-- Powiazanie alertow z konkretnym produktem
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'campaign_alerts'
|
||||||
|
AND COLUMN_NAME = 'product_id'
|
||||||
|
),
|
||||||
|
'DO 1',
|
||||||
|
'ALTER TABLE `campaign_alerts` ADD COLUMN `product_id` INT(11) NULL DEFAULT NULL AFTER `ad_group_external_id`'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = IF(
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'campaign_alerts'
|
||||||
|
AND INDEX_NAME = 'idx_alert_product'
|
||||||
|
),
|
||||||
|
'DO 1',
|
||||||
|
'CREATE INDEX `idx_alert_product` ON `campaign_alerts` (`product_id`)'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@@ -182,18 +182,6 @@ WHERE p.client_id = 2
|
|||||||
'shopify_PL_8901021','shopify_PL_8901022','shopify_PL_8901023','shopify_PL_8901024','shopify_PL_8901025'
|
'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
|
DELETE ph
|
||||||
FROM products_history ph
|
FROM products_history ph
|
||||||
JOIN products p ON p.id = ph.product_id
|
JOIN products p ON p.id = ph.product_id
|
||||||
@@ -387,26 +375,17 @@ GROUP BY p.id, p.name;
|
|||||||
|
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- 6. PRODUCTS_DATA (custom labels)
|
-- 6. PRODUCTS (custom labels)
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|
||||||
-- Bestsellery (wysoki ROAS + dużo konwersji)
|
-- Bestsellery (wysoki ROAS + dużo konwersji)
|
||||||
INSERT INTO products_data (product_id, custom_label_4) VALUES
|
UPDATE products SET custom_label_4 = 'bestseller' WHERE offer_id IN ( 'shopify_PL_8901001', 'shopify_PL_8901003', 'shopify_PL_8901010', 'shopify_PL_8901023' ) AND client_id = 2;
|
||||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901001' AND client_id = 2), 'bestseller'),
|
|
||||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901003' AND client_id = 2), 'bestseller'),
|
|
||||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901010' AND client_id = 2), 'bestseller'),
|
|
||||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901023' AND client_id = 2), 'bestseller');
|
|
||||||
|
|
||||||
-- Produkty PLA (w kampaniach Shopping)
|
-- Produkty PLA (w kampaniach Shopping)
|
||||||
INSERT INTO products_data (product_id, custom_label_4) VALUES
|
UPDATE products SET custom_label_4 = 'pla' WHERE offer_id IN ( 'shopify_PL_8901005', 'shopify_PL_8901006', 'shopify_PL_8901015' ) AND client_id = 2;
|
||||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901005' AND client_id = 2), 'pla'),
|
|
||||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901006' AND client_id = 2), 'pla'),
|
|
||||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901015' AND client_id = 2), 'pla');
|
|
||||||
|
|
||||||
-- Zombie (bardzo niskie wyświetlenia)
|
-- Zombie (bardzo niskie wyświetlenia)
|
||||||
INSERT INTO products_data (product_id, custom_label_4) VALUES
|
UPDATE products SET custom_label_4 = 'zombie' WHERE offer_id IN ( 'shopify_PL_8901014', 'shopify_PL_8901024' ) AND client_id = 2;
|
||||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901014' AND client_id = 2), 'zombie'),
|
|
||||||
((SELECT id FROM products WHERE offer_id = 'shopify_PL_8901024' AND client_id = 2), 'zombie');
|
|
||||||
|
|
||||||
|
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
|||||||
@@ -17,97 +17,185 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="campaigns-table-wrap">
|
<form method="post" action="/campaign_alerts/delete_selected" id="campaign_alerts_bulk_form">
|
||||||
<table class="table" id="campaign_alerts_table">
|
<input type="hidden" name="client_id" value="<?= (int) $this -> selected_client_id; ?>">
|
||||||
<thead>
|
<input type="hidden" name="page" value="<?= (int) $this -> page; ?>">
|
||||||
<tr>
|
|
||||||
<th>Data</th>
|
<div style="display:flex;justify-content:flex-end;margin-bottom:10px;">
|
||||||
<th>Klient</th>
|
<button type="submit" id="delete_selected_alerts_btn" class="btn btn-danger btn-sm" disabled>
|
||||||
<th>Kampania</th>
|
<i class="fa-solid fa-trash"></i> Usun zaznaczone (<span id="selected_alerts_count">0</span>)
|
||||||
<th>Grupa reklam</th>
|
</button>
|
||||||
<th>Komunikat</th>
|
</div>
|
||||||
</tr>
|
|
||||||
</thead>
|
<div class="campaigns-table-wrap">
|
||||||
<tbody>
|
<table class="table" id="campaign_alerts_table">
|
||||||
<?php if ( empty( $this -> alerts ) ): ?>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="text-center">Brak alertów.</td>
|
<th style="width:40px;text-align:center;">
|
||||||
|
<input type="checkbox" id="alerts_select_all" aria-label="Zaznacz wszystkie alerty">
|
||||||
|
</th>
|
||||||
|
<th>Data</th>
|
||||||
|
<th>Klient</th>
|
||||||
|
<th style="white-space:nowrap">Kampania</th>
|
||||||
|
<th>Grupa reklam</th>
|
||||||
|
<th>Komunikat</th>
|
||||||
</tr>
|
</tr>
|
||||||
<?php else: ?>
|
</thead>
|
||||||
<?php foreach ( $this -> alerts as $row ): ?>
|
<tbody>
|
||||||
<?php
|
<?php if ( empty( $this -> alerts ) ): ?>
|
||||||
$campaign_name = trim( (string) ( $row['campaign_name'] ?? '' ) );
|
|
||||||
if ( $campaign_name === '' )
|
|
||||||
{
|
|
||||||
$campaign_name = 'Kampania #' . (int) ( $row['campaign_external_id'] ?? 0 );
|
|
||||||
}
|
|
||||||
|
|
||||||
$ad_group_name = trim( (string) ( $row['ad_group_name'] ?? '' ) );
|
|
||||||
if ( $ad_group_name === '' )
|
|
||||||
{
|
|
||||||
$ad_group_name = 'Grupa reklam #' . (int) ( $row['ad_group_external_id'] ?? 0 );
|
|
||||||
}
|
|
||||||
|
|
||||||
$client_name = trim( (string) ( $row['client_name'] ?? '' ) );
|
|
||||||
if ( $client_name === '' )
|
|
||||||
{
|
|
||||||
$client_name = 'Klient #' . (int) ( $row['client_id'] ?? 0 );
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td style="white-space:nowrap"><?= htmlspecialchars( (string) ( $row['date_detected'] ?? '' ) ); ?></td>
|
<td colspan="6" class="text-center">Brak alertow.</td>
|
||||||
<td><?= htmlspecialchars( $client_name ); ?></td>
|
|
||||||
<td><?= htmlspecialchars( $campaign_name ); ?></td>
|
|
||||||
<td><?= htmlspecialchars( $ad_group_name ); ?></td>
|
|
||||||
<td><?= htmlspecialchars( (string) ( $row['message'] ?? '' ) ); ?></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php else: ?>
|
||||||
<?php endif; ?>
|
<?php foreach ( $this -> alerts as $row ): ?>
|
||||||
</tbody>
|
<?php
|
||||||
</table>
|
$alert_type = trim( (string) ( $row['alert_type'] ?? '' ) );
|
||||||
|
$campaign_name = trim( (string) ( $row['campaign_name'] ?? '' ) );
|
||||||
|
$ad_group_name = trim( (string) ( $row['ad_group_name'] ?? '' ) );
|
||||||
|
|
||||||
<?php if ( (int) $this -> total_pages > 1 ): ?>
|
if ( $alert_type === 'products_missing_in_merchant_center' )
|
||||||
<div class="dt-layout-row" style="display:flex !important;justify-content:flex-end;">
|
{
|
||||||
<div class="dt-paging">
|
$campaign_name = 'Produkt (Merchant Center)';
|
||||||
<nav>
|
$ad_group_name = '---';
|
||||||
<ul class="pagination">
|
}
|
||||||
<?php
|
else
|
||||||
$page = (int) $this -> page;
|
{
|
||||||
$total_pages = (int) $this -> total_pages;
|
if ( $campaign_name === '' )
|
||||||
$client_id = (int) $this -> selected_client_id;
|
{
|
||||||
$qs = $client_id > 0 ? '&client_id=' . $client_id : '';
|
$campaign_external_id = (int) ( $row['campaign_external_id'] ?? 0 );
|
||||||
|
$campaign_name = $campaign_external_id > 0 ? ( 'Kampania #' . $campaign_external_id ) : '--- konto ---';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $ad_group_name === '' )
|
||||||
|
{
|
||||||
|
$ad_group_external_id = (int) ( $row['ad_group_external_id'] ?? 0 );
|
||||||
|
$ad_group_name = $ad_group_external_id > 0 ? ( 'Grupa reklam #' . $ad_group_external_id ) : '---';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$client_name = trim( (string) ( $row['client_name'] ?? '' ) );
|
||||||
|
if ( $client_name === '' )
|
||||||
|
{
|
||||||
|
$client_name = 'Klient #' . (int) ( $row['client_id'] ?? 0 );
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<li class="page-item <?= $page <= 1 ? 'disabled' : ''; ?>">
|
<tr>
|
||||||
<a class="page-link" href="/campaign_alerts?page=<?= $page - 1; ?><?= $qs; ?>">«</a>
|
<td style="text-align:center;">
|
||||||
</li>
|
<input type="checkbox" name="alert_ids[]" value="<?= (int) ( $row['id'] ?? 0 ); ?>" class="alert-select-row" aria-label="Zaznacz alert #<?= (int) ( $row['id'] ?? 0 ); ?>">
|
||||||
<?php
|
</td>
|
||||||
$start_p = max( 1, $page - 2 );
|
<td style="white-space:nowrap"><?= htmlspecialchars( (string) ( $row['date_detected'] ?? '' ) ); ?></td>
|
||||||
$end_p = min( $total_pages, $page + 2 );
|
<td><?= htmlspecialchars( $client_name ); ?></td>
|
||||||
if ( $start_p > 1 ):
|
<td style="white-space:nowrap"><?= htmlspecialchars( $campaign_name ); ?></td>
|
||||||
?>
|
<td><?= htmlspecialchars( $ad_group_name ); ?></td>
|
||||||
<li class="page-item"><a class="page-link" href="/campaign_alerts?page=1<?= $qs; ?>">1</a></li>
|
<td><?= htmlspecialchars( (string) ( $row['message'] ?? '' ) ); ?></td>
|
||||||
<?php if ( $start_p > 2 ): ?>
|
</tr>
|
||||||
<li class="page-item disabled"><span class="page-link">...</span></li>
|
<?php endforeach; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
</tbody>
|
||||||
<?php for ( $i = $start_p; $i <= $end_p; $i++ ): ?>
|
</table>
|
||||||
<li class="page-item <?= $i === $page ? 'active' : ''; ?>">
|
|
||||||
<a class="page-link" href="/campaign_alerts?page=<?= $i; ?><?= $qs; ?>"><?= $i; ?></a>
|
<?php if ( (int) $this -> total_pages > 1 ): ?>
|
||||||
|
<div class="dt-layout-row" style="display:flex !important;justify-content:flex-end;">
|
||||||
|
<div class="dt-paging">
|
||||||
|
<nav>
|
||||||
|
<ul class="pagination">
|
||||||
|
<?php
|
||||||
|
$page = (int) $this -> page;
|
||||||
|
$total_pages = (int) $this -> total_pages;
|
||||||
|
$client_id = (int) $this -> selected_client_id;
|
||||||
|
$qs = $client_id > 0 ? '&client_id=' . $client_id : '';
|
||||||
|
?>
|
||||||
|
<li class="page-item <?= $page <= 1 ? 'disabled' : ''; ?>">
|
||||||
|
<a class="page-link" href="/campaign_alerts?page=<?= $page - 1; ?><?= $qs; ?>">«</a>
|
||||||
</li>
|
</li>
|
||||||
<?php endfor; ?>
|
<?php
|
||||||
<?php if ( $end_p < $total_pages ): ?>
|
$start_p = max( 1, $page - 2 );
|
||||||
<?php if ( $end_p < $total_pages - 1 ): ?>
|
$end_p = min( $total_pages, $page + 2 );
|
||||||
<li class="page-item disabled"><span class="page-link">...</span></li>
|
if ( $start_p > 1 ):
|
||||||
|
?>
|
||||||
|
<li class="page-item"><a class="page-link" href="/campaign_alerts?page=1<?= $qs; ?>">1</a></li>
|
||||||
|
<?php if ( $start_p > 2 ): ?>
|
||||||
|
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||||||
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<li class="page-item"><a class="page-link" href="/campaign_alerts?page=<?= $total_pages; ?><?= $qs; ?>"><?= $total_pages; ?></a></li>
|
<?php for ( $i = $start_p; $i <= $end_p; $i++ ): ?>
|
||||||
<?php endif; ?>
|
<li class="page-item <?= $i === $page ? 'active' : ''; ?>">
|
||||||
<li class="page-item <?= $page >= $total_pages ? 'disabled' : ''; ?>">
|
<a class="page-link" href="/campaign_alerts?page=<?= $i; ?><?= $qs; ?>"><?= $i; ?></a>
|
||||||
<a class="page-link" href="/campaign_alerts?page=<?= $page + 1; ?><?= $qs; ?>">»</a>
|
</li>
|
||||||
</li>
|
<?php endfor; ?>
|
||||||
</ul>
|
<?php if ( $end_p < $total_pages ): ?>
|
||||||
</nav>
|
<?php if ( $end_p < $total_pages - 1 ): ?>
|
||||||
|
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||||||
|
<?php endif; ?>
|
||||||
|
<li class="page-item"><a class="page-link" href="/campaign_alerts?page=<?= $total_pages; ?><?= $qs; ?>"><?= $total_pages; ?></a></li>
|
||||||
|
<?php endif; ?>
|
||||||
|
<li class="page-item <?= $page >= $total_pages ? 'disabled' : ''; ?>">
|
||||||
|
<a class="page-link" href="/campaign_alerts?page=<?= $page + 1; ?><?= $qs; ?>">»</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$( function()
|
||||||
|
{
|
||||||
|
var form = $( '#campaign_alerts_bulk_form' );
|
||||||
|
var select_all = $( '#alerts_select_all' );
|
||||||
|
var row_checkboxes = form.find( '.alert-select-row' );
|
||||||
|
var delete_button = $( '#delete_selected_alerts_btn' );
|
||||||
|
var selected_count = $( '#selected_alerts_count' );
|
||||||
|
|
||||||
|
function update_selected_state()
|
||||||
|
{
|
||||||
|
var checked = row_checkboxes.filter( ':checked' ).length;
|
||||||
|
var all = row_checkboxes.length;
|
||||||
|
|
||||||
|
selected_count.text( checked );
|
||||||
|
delete_button.prop( 'disabled', checked === 0 );
|
||||||
|
|
||||||
|
if ( all === 0 )
|
||||||
|
{
|
||||||
|
select_all.prop( 'checked', false );
|
||||||
|
select_all.prop( 'indeterminate', false );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
select_all.prop( 'checked', checked === all );
|
||||||
|
select_all.prop( 'indeterminate', checked > 0 && checked < all );
|
||||||
|
}
|
||||||
|
|
||||||
|
select_all.on( 'change', function()
|
||||||
|
{
|
||||||
|
row_checkboxes.prop( 'checked', this.checked );
|
||||||
|
update_selected_state();
|
||||||
|
} );
|
||||||
|
|
||||||
|
row_checkboxes.on( 'change', function()
|
||||||
|
{
|
||||||
|
update_selected_state();
|
||||||
|
} );
|
||||||
|
|
||||||
|
form.on( 'submit', function( e )
|
||||||
|
{
|
||||||
|
var checked = row_checkboxes.filter( ':checked' ).length;
|
||||||
|
|
||||||
|
if ( checked === 0 )
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !confirm( 'Czy na pewno usunac zaznaczone alerty (' + checked + ')?' ) )
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
update_selected_state();
|
||||||
|
} );
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -12,19 +12,28 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="width: 60px;">#ID</th>
|
<th style="width: 60px;">#ID</th>
|
||||||
<th>Nazwa klienta</th>
|
<th>Nazwa klienta</th>
|
||||||
|
<th style="width: 120px;">Status</th>
|
||||||
<th>Google Ads Customer ID</th>
|
<th>Google Ads Customer ID</th>
|
||||||
<th>Merchant Account ID</th>
|
<th>Merchant Account ID</th>
|
||||||
<th>Dane od</th>
|
<th>Dane od</th>
|
||||||
<th style="width: 160px;">Sync</th>
|
<th style="width: 190px;">Sync</th>
|
||||||
<th style="width: 160px; text-align: center;">Akcje</th>
|
<th style="width: 250px; text-align: center;">Akcje</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<?php if ( $this -> clients ): ?>
|
<?php if ( $this -> clients ): ?>
|
||||||
<?php foreach ( $this -> clients as $client ): ?>
|
<?php foreach ( $this -> clients as $client ): ?>
|
||||||
<tr data-id="<?= $client['id']; ?>">
|
<?php $is_client_active = (int) ( $client['active'] ?? 0 ) === 1; ?>
|
||||||
|
<tr data-id="<?= $client['id']; ?>" data-active="<?= $is_client_active ? 1 : 0; ?>">
|
||||||
<td class="client-id"><?= $client['id']; ?></td>
|
<td class="client-id"><?= $client['id']; ?></td>
|
||||||
<td class="client-name"><?= htmlspecialchars( $client['name'] ); ?></td>
|
<td class="client-name"><?= htmlspecialchars( $client['name'] ); ?></td>
|
||||||
|
<td data-status-for="<?= $client['id']; ?>">
|
||||||
|
<?php if ( $is_client_active ): ?>
|
||||||
|
<span class="text-success"><strong>Aktywny</strong></span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="text-danger"><strong>Nieaktywny</strong></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<?php if ( $client['google_ads_customer_id'] ): ?>
|
<?php if ( $client['google_ads_customer_id'] ): ?>
|
||||||
<span class="badge-id"><?= htmlspecialchars( $client['google_ads_customer_id'] ); ?></span>
|
<span class="badge-id"><?= htmlspecialchars( $client['google_ads_customer_id'] ); ?></span>
|
||||||
@@ -48,13 +57,21 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="client-sync" data-sync-id="<?= $client['id']; ?>"><span class="text-muted">—</span></td>
|
<td class="client-sync" data-sync-id="<?= $client['id']; ?>"><span class="text-muted">—</span></td>
|
||||||
<td class="actions-cell">
|
<td class="actions-cell">
|
||||||
|
<button type="button" class="btn-icon btn-icon-sync" onclick="toggleClientActive(<?= $client['id']; ?>, this)" title="<?= $is_client_active ? 'Dezaktywuj klienta' : 'Aktywuj klienta'; ?>">
|
||||||
|
<i class="fa-solid <?= $is_client_active ? 'fa-toggle-on' : 'fa-toggle-off'; ?>"></i>
|
||||||
|
</button>
|
||||||
<?php if ( $client['google_ads_customer_id'] ): ?>
|
<?php if ( $client['google_ads_customer_id'] ): ?>
|
||||||
<button type="button" class="btn-icon btn-icon-sync" onclick="syncClient(<?= $client['id']; ?>, 'campaigns', this)" title="Odśwież kampanie">
|
<button type="button" class="btn-icon btn-icon-sync client-sync-action" onclick="syncClient(<?= $client['id']; ?>, 'campaigns', this)" title="Odswiez kampanie" <?= $is_client_active ? '' : 'disabled'; ?>>
|
||||||
<i class="fa-solid fa-bullhorn"></i>
|
<i class="fa-solid fa-bullhorn"></i>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn-icon btn-icon-sync" onclick="syncClient(<?= $client['id']; ?>, 'products', this)" title="Odśwież produkty">
|
<button type="button" class="btn-icon btn-icon-sync client-sync-action" onclick="syncClient(<?= $client['id']; ?>, 'products', this)" title="Odswiez produkty" <?= $is_client_active ? '' : 'disabled'; ?>>
|
||||||
<i class="fa-solid fa-box-open"></i>
|
<i class="fa-solid fa-box-open"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<?php if ( !empty( $client['google_merchant_account_id'] ) ): ?>
|
||||||
|
<button type="button" class="btn-icon btn-icon-sync client-sync-action" onclick="syncClient(<?= $client['id']; ?>, 'campaigns_product_alerts_merchant', this)" title="Odswiez walidacje Merchant" <?= $is_client_active ? '' : 'disabled'; ?>>
|
||||||
|
<i class="fa-solid fa-store"></i>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<button type="button" class="btn-icon btn-icon-edit" onclick="editClient(<?= $client['id']; ?>)" title="Edytuj">
|
<button type="button" class="btn-icon btn-icon-edit" onclick="editClient(<?= $client['id']; ?>)" title="Edytuj">
|
||||||
<i class="fa-solid fa-pen"></i>
|
<i class="fa-solid fa-pen"></i>
|
||||||
@@ -67,7 +84,7 @@
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="empty-state">
|
<td colspan="8" class="empty-state">
|
||||||
<i class="fa-solid fa-building"></i>
|
<i class="fa-solid fa-building"></i>
|
||||||
<p>Brak klientów. Dodaj pierwszego klienta.</p>
|
<p>Brak klientów. Dodaj pierwszego klienta.</p>
|
||||||
</td>
|
</td>
|
||||||
@@ -92,6 +109,14 @@
|
|||||||
<label for="client-name">Nazwa klienta</label>
|
<label for="client-name">Nazwa klienta</label>
|
||||||
<input type="text" id="client-name" name="name" class="form-control" required placeholder="np. Firma XYZ" />
|
<input type="text" id="client-name" name="name" class="form-control" required placeholder="np. Firma XYZ" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-field">
|
||||||
|
<label for="client-active">Status klienta</label>
|
||||||
|
<select id="client-active" name="active" class="form-control">
|
||||||
|
<option value="1">Aktywny</option>
|
||||||
|
<option value="0">Nieaktywny</option>
|
||||||
|
</select>
|
||||||
|
<small class="text-muted">Nieaktywny klient nie jest synchronizowany.</small>
|
||||||
|
</div>
|
||||||
<div class="settings-field">
|
<div class="settings-field">
|
||||||
<label for="client-gads-id">Google Ads Customer ID</label>
|
<label for="client-gads-id">Google Ads Customer ID</label>
|
||||||
<input type="text" id="client-gads-id" name="google_ads_customer_id" class="form-control" placeholder="np. 123-456-7890 (opcjonalnie)" />
|
<input type="text" id="client-gads-id" name="google_ads_customer_id" class="form-control" placeholder="np. 123-456-7890 (opcjonalnie)" />
|
||||||
@@ -121,6 +146,7 @@ function openClientForm()
|
|||||||
$( '#client-modal-title' ).text( 'Dodaj klienta' );
|
$( '#client-modal-title' ).text( 'Dodaj klienta' );
|
||||||
$( '#client-id' ).val( '' );
|
$( '#client-id' ).val( '' );
|
||||||
$( '#client-name' ).val( '' );
|
$( '#client-name' ).val( '' );
|
||||||
|
$( '#client-active' ).val( '1' );
|
||||||
$( '#client-gads-id' ).val( '' );
|
$( '#client-gads-id' ).val( '' );
|
||||||
$( '#client-gmc-id' ).val( '' );
|
$( '#client-gmc-id' ).val( '' );
|
||||||
$( '#client-gads-start' ).val( '' );
|
$( '#client-gads-start' ).val( '' );
|
||||||
@@ -138,6 +164,7 @@ function editClient( id )
|
|||||||
$( '#client-modal-title' ).text( 'Edytuj klienta' );
|
$( '#client-modal-title' ).text( 'Edytuj klienta' );
|
||||||
$( '#client-id' ).val( data.id );
|
$( '#client-id' ).val( data.id );
|
||||||
$( '#client-name' ).val( data.name );
|
$( '#client-name' ).val( data.name );
|
||||||
|
$( '#client-active' ).val( parseInt( data.active, 10 ) === 0 ? '0' : '1' );
|
||||||
$( '#client-gads-id' ).val( data.google_ads_customer_id || '' );
|
$( '#client-gads-id' ).val( data.google_ads_customer_id || '' );
|
||||||
$( '#client-gmc-id' ).val( data.google_merchant_account_id || '' );
|
$( '#client-gmc-id' ).val( data.google_merchant_account_id || '' );
|
||||||
$( '#client-gads-start' ).val( data.google_ads_start_date || '' );
|
$( '#client-gads-start' ).val( data.google_ads_start_date || '' );
|
||||||
@@ -145,6 +172,67 @@ function editClient( id )
|
|||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateClientStatusUI( id, active, $btn )
|
||||||
|
{
|
||||||
|
var isActive = parseInt( active, 10 ) === 1;
|
||||||
|
var statusHtml = isActive
|
||||||
|
? '<span class="text-success"><strong>Aktywny</strong></span>'
|
||||||
|
: '<span class="text-danger"><strong>Nieaktywny</strong></span>';
|
||||||
|
|
||||||
|
$( 'tr[data-id="' + id + '"]' ).attr( 'data-active', isActive ? '1' : '0' );
|
||||||
|
$( 'td[data-status-for="' + id + '"]' ).html( statusHtml );
|
||||||
|
|
||||||
|
if ( $btn && $btn.length )
|
||||||
|
{
|
||||||
|
$btn.attr( 'title', isActive ? 'Dezaktywuj klienta' : 'Aktywuj klienta' );
|
||||||
|
$btn.find( 'i' ).attr( 'class', 'fa-solid ' + ( isActive ? 'fa-toggle-on' : 'fa-toggle-off' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$( 'tr[data-id="' + id + '"] .client-sync-action' ).prop( 'disabled', !isActive );
|
||||||
|
|
||||||
|
var $syncCell = $( '.client-sync[data-sync-id="' + id + '"]' );
|
||||||
|
if ( !isActive )
|
||||||
|
{
|
||||||
|
$syncCell.html( '<span class="text-muted">nieaktywny</span>' );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
loadSyncStatus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleClientActive( id, btn )
|
||||||
|
{
|
||||||
|
var $row = $( 'tr[data-id="' + id + '"]' );
|
||||||
|
var currentActive = parseInt( $row.attr( 'data-active' ), 10 ) === 1 ? 1 : 0;
|
||||||
|
var nextActive = currentActive === 1 ? 0 : 1;
|
||||||
|
var $btn = $( btn );
|
||||||
|
var $icon = $btn.find( 'i' );
|
||||||
|
var originalClass = $icon.attr( 'class' );
|
||||||
|
|
||||||
|
$btn.prop( 'disabled', true );
|
||||||
|
$icon.attr( 'class', 'fa-solid fa-spinner fa-spin' );
|
||||||
|
|
||||||
|
$.post( '/clients/set_active', { id: id, active: nextActive }, function( response ) {
|
||||||
|
var data = JSON.parse( response || '{}' );
|
||||||
|
|
||||||
|
$btn.prop( 'disabled', false );
|
||||||
|
$icon.attr( 'class', originalClass );
|
||||||
|
|
||||||
|
if ( data.success )
|
||||||
|
{
|
||||||
|
updateClientStatusUI( id, data.active, $btn );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.alert( {
|
||||||
|
title: 'Blad',
|
||||||
|
content: data.message || 'Nie udalo sie zmienic statusu klienta.',
|
||||||
|
type: 'red'
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
function syncClient( id, pipeline, btn )
|
function syncClient( id, pipeline, btn )
|
||||||
{
|
{
|
||||||
var $btn = $( btn );
|
var $btn = $( btn );
|
||||||
@@ -154,7 +242,11 @@ function syncClient( id, pipeline, btn )
|
|||||||
$btn.prop( 'disabled', true );
|
$btn.prop( 'disabled', true );
|
||||||
$icon.attr( 'class', 'fa-solid fa-spinner fa-spin' );
|
$icon.attr( 'class', 'fa-solid fa-spinner fa-spin' );
|
||||||
|
|
||||||
var labels = { campaigns: 'kampanii', products: 'produktów' };
|
var labels = {
|
||||||
|
campaigns: 'kampanii',
|
||||||
|
products: 'produktow',
|
||||||
|
campaigns_product_alerts_merchant: 'walidacji Merchant'
|
||||||
|
};
|
||||||
|
|
||||||
$.post( '/clients/force_sync', { id: id, pipeline: pipeline }, function( response )
|
$.post( '/clients/force_sync', { id: id, pipeline: pipeline }, function( response )
|
||||||
{
|
{
|
||||||
@@ -250,6 +342,13 @@ function loadSyncStatus()
|
|||||||
$( '.client-sync' ).each( function()
|
$( '.client-sync' ).each( function()
|
||||||
{
|
{
|
||||||
var $cell = $( this );
|
var $cell = $( this );
|
||||||
|
var isActive = parseInt( $cell.closest( 'tr' ).attr( 'data-active' ), 10 ) === 1;
|
||||||
|
if ( !isActive )
|
||||||
|
{
|
||||||
|
$cell.html( '<span class="text-muted">nieaktywny</span>' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var id = $cell.data( 'sync-id' );
|
var id = $cell.data( 'sync-id' );
|
||||||
var info = resp.data[ id ];
|
var info = resp.data[ id ];
|
||||||
|
|
||||||
@@ -262,6 +361,7 @@ function loadSyncStatus()
|
|||||||
var html = '<div class="client-sync-bars">';
|
var html = '<div class="client-sync-bars">';
|
||||||
if ( info.campaigns ) html += renderSyncBar( 'K:', info.campaigns[0], info.campaigns[1] );
|
if ( info.campaigns ) html += renderSyncBar( 'K:', info.campaigns[0], info.campaigns[1] );
|
||||||
if ( info.products ) html += renderSyncBar( 'P:', info.products[0], info.products[1] );
|
if ( info.products ) html += renderSyncBar( 'P:', info.products[0], info.products[1] );
|
||||||
|
if ( info.merchant ) html += renderSyncBar( 'M:', info.merchant[0], info.merchant[1] );
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
$cell.html( html );
|
$cell.html( html );
|
||||||
@@ -274,3 +374,4 @@ $( document ).ready( function() {
|
|||||||
setInterval( loadSyncStatus, 15000 );
|
setInterval( loadSyncStatus, 15000 );
|
||||||
} );
|
} );
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -22,9 +22,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="filter-group filter-group-ad-group">
|
<div class="filter-group filter-group-ad-group">
|
||||||
<label for="products_ad_group_id"><i class="fa-solid fa-layer-group"></i> Grupa reklam</label>
|
<label for="products_ad_group_id"><i class="fa-solid fa-layer-group"></i> Grupa reklam</label>
|
||||||
<select id="products_ad_group_id" name="products_ad_group_id" class="form-control">
|
<div class="ad-group-filter-actions">
|
||||||
<option value="">- wszystkie grupy -</option>
|
<select id="products_ad_group_id" name="products_ad_group_id" class="form-control">
|
||||||
</select>
|
<option value="">- wszystkie grupy -</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" id="delete-products-ad-group" class="btn-icon btn-icon-delete" title="Usun wybrana grupe reklam" disabled>
|
||||||
|
<i class="fa-solid fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group filter-group-roas">
|
<div class="filter-group filter-group-roas">
|
||||||
<label for="bestseller_min_roas"><i class="fa-solid fa-star"></i> Bestseller min ROAS</label>
|
<label for="bestseller_min_roas"><i class="fa-solid fa-star"></i> Bestseller min ROAS</label>
|
||||||
@@ -39,6 +44,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details id="products_scope_alerts_box" class="products-scope-alerts hide">
|
||||||
|
<summary>
|
||||||
|
<i class="fa-solid fa-triangle-exclamation"></i>
|
||||||
|
Alerty dla wybranej kampanii (i opcjonalnie grupy reklam)
|
||||||
|
(<span id="products_scope_alerts_count">0</span>)
|
||||||
|
</summary>
|
||||||
|
<div class="products-scope-alerts-list" id="products_scope_alerts_list"></div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details id="products_zero_impressions_box" class="products-scope-alerts hide">
|
||||||
|
<summary>
|
||||||
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
|
Produkty do sprawdzenia (0 wyswietlen w ostatnich 30 dniach)
|
||||||
|
(<span id="products_zero_impressions_count">0</span>)
|
||||||
|
</summary>
|
||||||
|
<div class="products-scope-alerts-list">
|
||||||
|
<select id="products_zero_impressions_select" class="form-control" size="8"></select>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<!-- Akcje bulk -->
|
<!-- Akcje bulk -->
|
||||||
<div class="products-actions">
|
<div class="products-actions">
|
||||||
<button type="button" class="btn btn-danger btn-sm" id="delete-selected-products" disabled>
|
<button type="button" class="btn btn-danger btn-sm" id="delete-selected-products" disabled>
|
||||||
@@ -351,6 +376,151 @@ $( function()
|
|||||||
products_table.ajax.reload( null, false );
|
products_table.ajax.reload( null, false );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function submit_delete_campaign_ad_group( campaign_id, ad_group_id, delete_scope )
|
||||||
|
{
|
||||||
|
function parse_json_loose( raw )
|
||||||
|
{
|
||||||
|
if ( typeof raw !== 'string' || !raw )
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = $.trim( raw );
|
||||||
|
if ( !text )
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JSON.parse( text );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
var start = text.indexOf( '{' );
|
||||||
|
var end = text.lastIndexOf( '}' );
|
||||||
|
|
||||||
|
if ( start !== -1 && end > start )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JSON.parse( text.substring( start, end + 1 ) );
|
||||||
|
}
|
||||||
|
catch ( e2 ) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_success( message )
|
||||||
|
{
|
||||||
|
show_toast( message || 'Grupa reklam zostala usunieta.', 'success' );
|
||||||
|
localStorage.removeItem( 'products_ad_group_id' );
|
||||||
|
load_products_ad_groups( campaign_id, '' ).done( function() {
|
||||||
|
$.when( load_scope_alerts(), load_zero_impressions_products() ).always( function() {
|
||||||
|
reload_products_table();
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_if_ad_group_still_exists()
|
||||||
|
{
|
||||||
|
var deferred = $.Deferred();
|
||||||
|
|
||||||
|
if ( !campaign_id || !ad_group_id )
|
||||||
|
{
|
||||||
|
deferred.resolve( true );
|
||||||
|
return deferred.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/products/get_campaign_ad_groups/campaign_id=' + campaign_id,
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json'
|
||||||
|
}).done( function( res ) {
|
||||||
|
var still_exists = false;
|
||||||
|
|
||||||
|
( res.ad_groups || [] ).forEach( function( row ) {
|
||||||
|
if ( String( row.id || '' ) === String( ad_group_id ) )
|
||||||
|
{
|
||||||
|
still_exists = true;
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
deferred.resolve( still_exists );
|
||||||
|
}).fail( function() {
|
||||||
|
deferred.resolve( true );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return deferred.promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
var request_data = {
|
||||||
|
campaign_id: campaign_id,
|
||||||
|
ad_group_id: ad_group_id,
|
||||||
|
delete_scope: delete_scope
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/products/delete_campaign_ad_group/',
|
||||||
|
type: 'POST',
|
||||||
|
data: request_data,
|
||||||
|
success: function( response )
|
||||||
|
{
|
||||||
|
var res = ( typeof response === 'object' && response !== null )
|
||||||
|
? response
|
||||||
|
: parse_json_loose( response );
|
||||||
|
|
||||||
|
if ( res && res.status === 'ok' )
|
||||||
|
{
|
||||||
|
handle_success( res.message );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
check_if_ad_group_still_exists().done( function( still_exists ) {
|
||||||
|
if ( !still_exists )
|
||||||
|
{
|
||||||
|
handle_success( ( res && res.message ) ? res.message : 'Grupa reklam zostala usunieta.' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.alert({
|
||||||
|
title: 'Blad',
|
||||||
|
content: ( res && res.message ) ? res.message : 'Nie udalo sie usunac grupy reklam.',
|
||||||
|
type: 'red'
|
||||||
|
});
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function( jqXHR )
|
||||||
|
{
|
||||||
|
var res = parse_json_loose( jqXHR && jqXHR.responseText ? jqXHR.responseText : '' );
|
||||||
|
|
||||||
|
if ( res && res.status === 'ok' )
|
||||||
|
{
|
||||||
|
handle_success( res.message );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
check_if_ad_group_still_exists().done( function( still_exists ) {
|
||||||
|
if ( !still_exists )
|
||||||
|
{
|
||||||
|
handle_success( ( res && res.message ) ? res.message : 'Grupa reklam zostala usunieta.' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.alert({
|
||||||
|
title: 'Blad',
|
||||||
|
content: ( res && res.message ) ? res.message : 'Wystapil blad podczas usuwania grupy reklam.',
|
||||||
|
type: 'red'
|
||||||
|
});
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function load_client_bestseller_min_roas( client_id )
|
function load_client_bestseller_min_roas( client_id )
|
||||||
{
|
{
|
||||||
if ( !client_id )
|
if ( !client_id )
|
||||||
@@ -386,16 +556,35 @@ $( function()
|
|||||||
dataType: 'json'
|
dataType: 'json'
|
||||||
}).done( function( res ) {
|
}).done( function( res ) {
|
||||||
( res.campaigns || [] ).forEach( function( row ) {
|
( res.campaigns || [] ).forEach( function( row ) {
|
||||||
$campaign.append( '<option value="' + row.id + '">' + row.campaign_name + '</option>' );
|
var channel_type = String( row.advertising_channel_type || '' ).toUpperCase();
|
||||||
|
$campaign.append(
|
||||||
|
'<option value="' + row.id + '" data-channel-type="' + channel_type + '">' + escape_html( row.campaign_name || '' ) + '</option>'
|
||||||
|
);
|
||||||
} );
|
} );
|
||||||
|
|
||||||
if ( selected_campaign_id && $campaign.find( 'option[value="' + selected_campaign_id + '"]' ).length )
|
if ( selected_campaign_id && $campaign.find( 'option[value="' + selected_campaign_id + '"]' ).length )
|
||||||
{
|
{
|
||||||
$campaign.val( selected_campaign_id );
|
$campaign.val( selected_campaign_id );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_delete_ad_group_button_state();
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get_selected_products_campaign_channel_type()
|
||||||
|
{
|
||||||
|
var campaign_id = $( '#products_campaign_id' ).val() || '';
|
||||||
|
|
||||||
|
if ( !campaign_id )
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(
|
||||||
|
$( '#products_campaign_id option[value="' + campaign_id + '"]' ).attr( 'data-channel-type' ) || ''
|
||||||
|
).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
function load_products_ad_groups( campaign_id, selected_ad_group_id )
|
function load_products_ad_groups( campaign_id, selected_ad_group_id )
|
||||||
{
|
{
|
||||||
var $ad_group = $( '#products_ad_group_id' );
|
var $ad_group = $( '#products_ad_group_id' );
|
||||||
@@ -403,6 +592,7 @@ $( function()
|
|||||||
|
|
||||||
if ( !campaign_id )
|
if ( !campaign_id )
|
||||||
{
|
{
|
||||||
|
update_delete_ad_group_button_state();
|
||||||
return $.Deferred().resolve().promise();
|
return $.Deferred().resolve().promise();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,6 +609,168 @@ $( function()
|
|||||||
{
|
{
|
||||||
$ad_group.val( selected_ad_group_id );
|
$ad_group.val( selected_ad_group_id );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_delete_ad_group_button_state();
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_delete_ad_group_button_state()
|
||||||
|
{
|
||||||
|
var campaign_channel_type = get_selected_products_campaign_channel_type();
|
||||||
|
var ad_group_id = $( '#products_ad_group_id' ).val() || '';
|
||||||
|
var is_shopping_campaign = campaign_channel_type === 'SHOPPING';
|
||||||
|
var can_delete = is_shopping_campaign && ad_group_id !== '';
|
||||||
|
var $btn = $( '#delete-products-ad-group' );
|
||||||
|
|
||||||
|
$btn.prop( 'disabled', !can_delete );
|
||||||
|
|
||||||
|
if ( !is_shopping_campaign )
|
||||||
|
{
|
||||||
|
$btn.attr( 'title', 'Opcja dostepna tylko dla kampanii produktowych (Shopping)' );
|
||||||
|
}
|
||||||
|
else if ( ad_group_id === '' )
|
||||||
|
{
|
||||||
|
$btn.attr( 'title', 'Wybierz grupe reklam do usuniecia' );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
$btn.attr( 'title', 'Usun wybrana grupe reklam' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_scope_alerts_box( alerts )
|
||||||
|
{
|
||||||
|
var $box = $( '#products_scope_alerts_box' );
|
||||||
|
var $count = $( '#products_scope_alerts_count' );
|
||||||
|
var $list = $( '#products_scope_alerts_list' );
|
||||||
|
var rows = Array.isArray( alerts ) ? alerts : [];
|
||||||
|
|
||||||
|
if ( !rows.length )
|
||||||
|
{
|
||||||
|
$count.text( '0' );
|
||||||
|
$list.empty();
|
||||||
|
$box.addClass( 'hide' ).removeAttr( 'open' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
rows.forEach( function( row ) {
|
||||||
|
var date_text = row.date_detected || row.date_add || '';
|
||||||
|
var type_text = row.alert_type ? String( row.alert_type ) : '';
|
||||||
|
var message_text = row.message ? String( row.message ) : '';
|
||||||
|
|
||||||
|
html += ''
|
||||||
|
+ '<div class="products-scope-alert-item">'
|
||||||
|
+ '<div class="products-scope-alert-meta">'
|
||||||
|
+ '<span class="products-scope-alert-date">' + escape_html( date_text ) + '</span>'
|
||||||
|
+ ( type_text ? '<span class="products-scope-alert-type">' + escape_html( type_text ) + '</span>' : '' )
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="products-scope-alert-message">' + escape_html( message_text ) + '</div>'
|
||||||
|
+ '</div>';
|
||||||
|
} );
|
||||||
|
|
||||||
|
$count.text( rows.length );
|
||||||
|
$list.html( html );
|
||||||
|
$box.removeClass( 'hide' ).attr( 'open', 'open' );
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_scope_alerts()
|
||||||
|
{
|
||||||
|
var client_id = $( '#client_id' ).val() || '';
|
||||||
|
var campaign_id = $( '#products_campaign_id' ).val() || '';
|
||||||
|
var ad_group_id = $( '#products_ad_group_id' ).val() || '';
|
||||||
|
|
||||||
|
if ( !client_id || !campaign_id )
|
||||||
|
{
|
||||||
|
render_scope_alerts_box( [] );
|
||||||
|
return $.Deferred().resolve().promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $.ajax({
|
||||||
|
url: '/products/get_scope_alerts/',
|
||||||
|
type: 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
data: {
|
||||||
|
client_id: client_id,
|
||||||
|
campaign_id: campaign_id,
|
||||||
|
ad_group_id: ad_group_id
|
||||||
|
}
|
||||||
|
}).done( function( res ) {
|
||||||
|
if ( res && res.status === 'ok' )
|
||||||
|
{
|
||||||
|
render_scope_alerts_box( res.alerts || [] );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
render_scope_alerts_box( [] );
|
||||||
|
}
|
||||||
|
}).fail( function() {
|
||||||
|
render_scope_alerts_box( [] );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
function render_zero_impressions_box( products )
|
||||||
|
{
|
||||||
|
var $box = $( '#products_zero_impressions_box' );
|
||||||
|
var $count = $( '#products_zero_impressions_count' );
|
||||||
|
var $select = $( '#products_zero_impressions_select' );
|
||||||
|
var rows = Array.isArray( products ) ? products : [];
|
||||||
|
|
||||||
|
$select.empty();
|
||||||
|
|
||||||
|
if ( !rows.length )
|
||||||
|
{
|
||||||
|
$count.text( '0' );
|
||||||
|
$box.addClass( 'hide' ).removeAttr( 'open' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.forEach( function( row ) {
|
||||||
|
var product_id = parseInt( row.product_id || 0, 10 );
|
||||||
|
var offer_id = row.offer_id ? String( row.offer_id ) : '';
|
||||||
|
var product_name = row.name ? String( row.name ) : '';
|
||||||
|
var label = '#' + product_id + ( offer_id ? ' | ' + offer_id : '' ) + ' | ' + product_name;
|
||||||
|
|
||||||
|
$select.append( '<option value="' + product_id + '">' + escape_html( label ) + '</option>' );
|
||||||
|
} );
|
||||||
|
|
||||||
|
$count.text( rows.length );
|
||||||
|
$box.removeClass( 'hide' );
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_zero_impressions_products()
|
||||||
|
{
|
||||||
|
var client_id = $( '#client_id' ).val() || '';
|
||||||
|
var campaign_id = $( '#products_campaign_id' ).val() || '';
|
||||||
|
var ad_group_id = $( '#products_ad_group_id' ).val() || '';
|
||||||
|
|
||||||
|
if ( !client_id || !campaign_id )
|
||||||
|
{
|
||||||
|
render_zero_impressions_box( [] );
|
||||||
|
return $.Deferred().resolve().promise();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $.ajax({
|
||||||
|
url: '/products/get_products_without_impressions_30/',
|
||||||
|
type: 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
data: {
|
||||||
|
client_id: client_id,
|
||||||
|
campaign_id: campaign_id,
|
||||||
|
ad_group_id: ad_group_id
|
||||||
|
}
|
||||||
|
}).done( function( res ) {
|
||||||
|
if ( res && res.status === 'ok' )
|
||||||
|
{
|
||||||
|
render_zero_impressions_box( res.products || [] );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
render_zero_impressions_box( [] );
|
||||||
|
}
|
||||||
|
}).fail( function() {
|
||||||
|
render_zero_impressions_box( [] );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,11 +1082,15 @@ $( function()
|
|||||||
localStorage.setItem( 'products_client_id', client_id );
|
localStorage.setItem( 'products_client_id', client_id );
|
||||||
localStorage.removeItem( 'products_campaign_id' );
|
localStorage.removeItem( 'products_campaign_id' );
|
||||||
localStorage.removeItem( 'products_ad_group_id' );
|
localStorage.removeItem( 'products_ad_group_id' );
|
||||||
|
update_delete_ad_group_button_state();
|
||||||
|
|
||||||
load_client_bestseller_min_roas( client_id );
|
load_client_bestseller_min_roas( client_id );
|
||||||
load_products_campaigns( client_id, '' ).done( function() {
|
load_products_campaigns( client_id, '' ).done( function() {
|
||||||
load_products_ad_groups( '', '' ).done( function() {
|
load_products_ad_groups( '', '' ).done( function() {
|
||||||
reload_products_table();
|
update_delete_ad_group_button_state();
|
||||||
|
$.when( load_scope_alerts(), load_zero_impressions_products() ).always( function() {
|
||||||
|
reload_products_table();
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
});
|
});
|
||||||
@@ -744,9 +1100,12 @@ $( function()
|
|||||||
var campaign_id = $( this ).val() || '';
|
var campaign_id = $( this ).val() || '';
|
||||||
localStorage.setItem( 'products_campaign_id', campaign_id );
|
localStorage.setItem( 'products_campaign_id', campaign_id );
|
||||||
localStorage.removeItem( 'products_ad_group_id' );
|
localStorage.removeItem( 'products_ad_group_id' );
|
||||||
|
update_delete_ad_group_button_state();
|
||||||
|
|
||||||
load_products_ad_groups( campaign_id, '' ).done( function() {
|
load_products_ad_groups( campaign_id, '' ).done( function() {
|
||||||
reload_products_table();
|
$.when( load_scope_alerts(), load_zero_impressions_products() ).always( function() {
|
||||||
|
reload_products_table();
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -754,7 +1113,71 @@ $( function()
|
|||||||
{
|
{
|
||||||
var ad_group_id = $( this ).val() || '';
|
var ad_group_id = $( this ).val() || '';
|
||||||
localStorage.setItem( 'products_ad_group_id', ad_group_id );
|
localStorage.setItem( 'products_ad_group_id', ad_group_id );
|
||||||
reload_products_table();
|
update_delete_ad_group_button_state();
|
||||||
|
$.when( load_scope_alerts(), load_zero_impressions_products() ).always( function() {
|
||||||
|
reload_products_table();
|
||||||
|
} );
|
||||||
|
});
|
||||||
|
|
||||||
|
$( 'body' ).on( 'click', '#delete-products-ad-group', function( e )
|
||||||
|
{
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var campaign_id = $( '#products_campaign_id' ).val() || '';
|
||||||
|
var ad_group_id = $( '#products_ad_group_id' ).val() || '';
|
||||||
|
var campaign_channel_type = get_selected_products_campaign_channel_type();
|
||||||
|
|
||||||
|
if ( campaign_channel_type !== 'SHOPPING' )
|
||||||
|
{
|
||||||
|
$.alert({
|
||||||
|
title: 'Niedostepne',
|
||||||
|
content: 'Usuwanie grup reklam jest dostepne tylko dla kampanii produktowych (Shopping).',
|
||||||
|
type: 'orange'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !campaign_id || !ad_group_id )
|
||||||
|
{
|
||||||
|
$.alert({
|
||||||
|
title: 'Brak wyboru',
|
||||||
|
content: 'Wybierz kampanie i grupe reklam do usuniecia.',
|
||||||
|
type: 'orange'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ad_group_name = $.trim( $( '#products_ad_group_id option:selected' ).text() || '' );
|
||||||
|
var campaign_name = $.trim( $( '#products_campaign_id option:selected' ).text() || '' );
|
||||||
|
|
||||||
|
$.confirm({
|
||||||
|
title: 'Usuwanie grupy reklam',
|
||||||
|
content:
|
||||||
|
'Jak chcesz usunac grupe reklam <strong>' + escape_html( ad_group_name ) + '</strong> z kampanii <strong>' + escape_html( campaign_name ) + '</strong>?'
|
||||||
|
+ '<br><br><small>Opcja API usunie grupe reklam rowniez w Google Ads.</small>',
|
||||||
|
type: 'red',
|
||||||
|
buttons: {
|
||||||
|
local: {
|
||||||
|
text: 'Tylko lokalnie',
|
||||||
|
btnClass: 'btn-default',
|
||||||
|
action: function()
|
||||||
|
{
|
||||||
|
return submit_delete_campaign_ad_group( campaign_id, ad_group_id, 'local' );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
google: {
|
||||||
|
text: 'Lokalnie + Google Ads',
|
||||||
|
btnClass: 'btn-red',
|
||||||
|
action: function()
|
||||||
|
{
|
||||||
|
return submit_delete_campaign_ad_group( campaign_id, ad_group_id, 'google' );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
text: 'Anuluj'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
var savedClient = localStorage.getItem( 'products_client_id' ) || '';
|
var savedClient = localStorage.getItem( 'products_client_id' ) || '';
|
||||||
@@ -770,7 +1193,10 @@ $( function()
|
|||||||
load_products_campaigns( $( '#client_id' ).val() || '', savedCampaign ).done( function() {
|
load_products_campaigns( $( '#client_id' ).val() || '', savedCampaign ).done( function() {
|
||||||
var selected_campaign_id = $( '#products_campaign_id' ).val() || '';
|
var selected_campaign_id = $( '#products_campaign_id' ).val() || '';
|
||||||
load_products_ad_groups( selected_campaign_id, savedAdGroup ).done( function() {
|
load_products_ad_groups( selected_campaign_id, savedAdGroup ).done( function() {
|
||||||
reload_products_table();
|
update_delete_ad_group_button_state();
|
||||||
|
$.when( load_scope_alerts(), load_zero_impressions_products() ).always( function() {
|
||||||
|
reload_products_table();
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
<strong><?= htmlspecialchars( (string) ( $row['name'] ?? 'Cron' ) ); ?></strong>
|
<strong><?= htmlspecialchars( (string) ( $row['name'] ?? 'Cron' ) ); ?></strong>
|
||||||
<small>Ostatnio: <span data-cron-url-last-invoked><?= htmlspecialchars( (string) ( $row['last_invoked_at'] ?? 'Brak danych' ) ); ?></span></small>
|
<small>Ostatnio: <span data-cron-url-last-invoked><?= htmlspecialchars( (string) ( $row['last_invoked_at'] ?? 'Brak danych' ) ); ?></span></small>
|
||||||
</div>
|
</div>
|
||||||
|
<small class="cron-url-plan" data-cron-url-plan><?= htmlspecialchars( (string) ( $row['plan'] ?? '' ) ); ?></small>
|
||||||
<code><?= htmlspecialchars( (string) ( $row['url'] ?? '' ) ); ?></code>
|
<code><?= htmlspecialchars( (string) ( $row['url'] ?? '' ) ); ?></code>
|
||||||
</div>
|
</div>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
@@ -382,6 +383,12 @@
|
|||||||
{
|
{
|
||||||
last_invoked.textContent = row && row.last_invoked_at ? row.last_invoked_at : 'Brak danych';
|
last_invoked.textContent = row && row.last_invoked_at ? row.last_invoked_at : 'Brak danych';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var plan = container.querySelector( '[data-cron-url-plan]' );
|
||||||
|
if ( plan )
|
||||||
|
{
|
||||||
|
plan.textContent = row && row.plan ? row.plan : '';
|
||||||
|
}
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
54
tmp/campaign_alerts_debug.log
Normal file
54
tmp/campaign_alerts_debug.log
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{"ts":"2026-02-20 10:51:13","stage":"sync_product_campaign_alerts_for_client:start","context":{"client_id":2,"customer_id":"941-605-1782","date_sync":"2026-02-20","campaigns_db_map_count":6,"ad_group_db_map_count":20}}
|
||||||
|
{"ts":"2026-02-20 10:51:15","stage":"google_ads:get_shopping_ad_group_offer_ids","context":{"result_type":"false","rows_count":0,"sample":[]}}
|
||||||
|
{"ts":"2026-02-20 10:51:16","stage":"google_ads:get_shopping_ad_group_offer_ids_from_performance","context":{"result_type":"array","rows_count":147,"sample":[{"campaign_id":22389325355,"campaign_name":"[PLA] chrzest święty","ad_group_id":174195978930,"ad_group_name":"Pamiątka Pierwszej Komunii Świętej Pudełko na pieniądze - Kielich","offer_ids":[1773]},{"campaign_id":22389325355,"campaign_name":"[PLA] chrzest święty","ad_group_id":175963483182,"ad_group_name":"Pamiątka Pierwszej Komunii Świętej Pudełko na pieniądze - Dłonie","offer_ids":[1772]},{"campaign_id":22389325355,"campaign_name":"[PLA] chrzest święty","ad_group_id":177111077493,"ad_group_name":"Prośba o bycie Matką Chrzestną, grawerowana, z imieniem dziecka, Misiek - Kolor serduszka: czerwony","offer_ids":[921]}]}}
|
||||||
|
{"ts":"2026-02-20 10:51:16","stage":"merchant:offer_ids_to_verify","context":{"count":41,"sample":[1675,1467,1063,1678,910,1500,944,1652,1738,1072,1672,2025,934,928,1316,1319,1313,1715,1677,1767,1340,1778,1067,1721,1368,1339,949,1345,1824,1536]}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"merchant:get_merchant_products_for_offer_ids","context":{"merchant_account_id":"729206752","request_count":41,"response_type":"array","response_count":41,"response_sample_keys":[1467,1767,1672,1652,1067,1368,1510,1675,1824,1536,1321,1340,1720,1063,1339,1316,949,1436,1715,1722,1778,1874,1723,928,1319,1345,1313,1738,1677,1678]}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"db:local_offer_ids","context":{"count":592,"sample":[1778,2013,944,1536,2016,1928,2014,1720,1951,1064,1978,906,921,1993,1942,1812,1813,1061,1596,1509,911,1360,1569,1662,2080,1581,1783,1580,2071,1849]}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|178830027730","campaign_external_id":22855767992,"ad_group_external_id":178830027730,"offer_ids":["1675"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|182023998383","campaign_external_id":22855767992,"ad_group_external_id":182023998383,"offer_ids":["1467"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|182184163854","campaign_external_id":22855767992,"ad_group_external_id":182184163854,"offer_ids":["1063"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|182329510303","campaign_external_id":22881177775,"ad_group_external_id":182329510303,"offer_ids":["1678"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|182437144463","campaign_external_id":22855767992,"ad_group_external_id":182437144463,"offer_ids":["910"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|183091891829","campaign_external_id":22855767992,"ad_group_external_id":183091891829,"offer_ids":["1500"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|183523029996","campaign_external_id":22855767992,"ad_group_external_id":183523029996,"offer_ids":["944"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|184090691740","campaign_external_id":22855767992,"ad_group_external_id":184090691740,"offer_ids":["1652"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|184491635422","campaign_external_id":22881177775,"ad_group_external_id":184491635422,"offer_ids":["1738"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|184781679245","campaign_external_id":22881177775,"ad_group_external_id":184781679245,"offer_ids":["1072"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|185056281737","campaign_external_id":22881177775,"ad_group_external_id":185056281737,"offer_ids":["1672"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|186478211005","campaign_external_id":22881177775,"ad_group_external_id":186478211005,"offer_ids":["2025"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|187193909207","campaign_external_id":22855767992,"ad_group_external_id":187193909207,"offer_ids":["934"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|187202566030","campaign_external_id":22855767992,"ad_group_external_id":187202566030,"offer_ids":["928"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|187617501346","campaign_external_id":22881177775,"ad_group_external_id":187617501346,"offer_ids":["1316"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|187617773426","campaign_external_id":22881177775,"ad_group_external_id":187617773426,"offer_ids":["1319"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|187737512843","campaign_external_id":22881177775,"ad_group_external_id":187737512843,"offer_ids":["1313"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|188437646135","campaign_external_id":22881177775,"ad_group_external_id":188437646135,"offer_ids":["1715"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|188844670995","campaign_external_id":22881177775,"ad_group_external_id":188844670995,"offer_ids":["1677"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23169295644|189029743564","campaign_external_id":23169295644,"ad_group_external_id":189029743564,"offer_ids":["1767"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|189133168538","campaign_external_id":22881177775,"ad_group_external_id":189133168538,"offer_ids":["1340"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|189174321780","campaign_external_id":22881177775,"ad_group_external_id":189174321780,"offer_ids":["1672"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|189377483261","campaign_external_id":22855767992,"ad_group_external_id":189377483261,"offer_ids":["1778"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|189393194528","campaign_external_id":22855767992,"ad_group_external_id":189393194528,"offer_ids":["1067"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|189782782720","campaign_external_id":22881177775,"ad_group_external_id":189782782720,"offer_ids":["1721"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|190157433529","campaign_external_id":22881177775,"ad_group_external_id":190157433529,"offer_ids":["1368"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|190254577916","campaign_external_id":22855767992,"ad_group_external_id":190254577916,"offer_ids":["1738"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|190981545071","campaign_external_id":22881177775,"ad_group_external_id":190981545071,"offer_ids":["1339"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|191005617337","campaign_external_id":23554819089,"ad_group_external_id":191005617337,"offer_ids":["944"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|191005617537","campaign_external_id":23554819089,"ad_group_external_id":191005617537,"offer_ids":["949"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|191005617577","campaign_external_id":23554819089,"ad_group_external_id":191005617577,"offer_ids":["1345"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|191005617737","campaign_external_id":23554819089,"ad_group_external_id":191005617737,"offer_ids":["1824"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|191005618937","campaign_external_id":23554819089,"ad_group_external_id":191005618937,"offer_ids":["1536"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|191170651834","campaign_external_id":22881177775,"ad_group_external_id":191170651834,"offer_ids":["1324"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|191185839027","campaign_external_id":22881177775,"ad_group_external_id":191185839027,"offer_ids":["1321"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|191596793445","campaign_external_id":22855767992,"ad_group_external_id":191596793445,"offer_ids":["2016"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|191936594094","campaign_external_id":23554819089,"ad_group_external_id":191936594094,"offer_ids":["1928"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|191938697986","campaign_external_id":22855767992,"ad_group_external_id":191938697986,"offer_ids":["949"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|192480293437","campaign_external_id":22855767992,"ad_group_external_id":192480293437,"offer_ids":["1536"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|192764842295","campaign_external_id":22855767992,"ad_group_external_id":192764842295,"offer_ids":["1824"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|193141002373","campaign_external_id":23554819089,"ad_group_external_id":193141002373,"offer_ids":["1915"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|193760921208","campaign_external_id":22881177775,"ad_group_external_id":193760921208,"offer_ids":["1723"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":0}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|194005981900","campaign_external_id":23554819089,"ad_group_external_id":194005981900,"offer_ids":["1874"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|194928085664","campaign_external_id":22881177775,"ad_group_external_id":194928085664,"offer_ids":["1722"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22881177775|195763522708","campaign_external_id":22881177775,"ad_group_external_id":195763522708,"offer_ids":["1436"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"22855767992|197096047150","campaign_external_id":22855767992,"ad_group_external_id":197096047150,"offer_ids":["1720"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"scope:verification","context":{"scope_key":"23554819089|198928421011","campaign_external_id":23554819089,"ad_group_external_id":198928421011,"offer_ids":["1510"],"active_offer_count":1,"orphaned_offer_ids":[],"local_known_offer_count":1}}
|
||||||
|
{"ts":"2026-02-20 10:51:18","stage":"alerts:cleanup","context":{"existing_count":7,"cleaned_count":0,"problematic_without_active_product":0,"problematic_with_orphaned_offers":0,"problematic_without_detected_product":7}}
|
||||||
32
tmp/debug_clients.php
Normal file
32
tmp/debug_clients.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', '1');
|
||||||
|
echo "START\n";
|
||||||
|
require 'config.php';
|
||||||
|
require 'libraries/medoo/medoo.php';
|
||||||
|
try {
|
||||||
|
$mdb = new medoo([
|
||||||
|
'database_type' => 'mysql',
|
||||||
|
'database_name' => $database['name'],
|
||||||
|
'server' => $database['host'],
|
||||||
|
'username' => $database['user'],
|
||||||
|
'password' => $database['password'],
|
||||||
|
'charset' => 'utf8'
|
||||||
|
]);
|
||||||
|
echo "CONNECTED\n";
|
||||||
|
$cols = $mdb->query("SHOW COLUMNS FROM clients")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
echo "COLUMNS=" . count($cols) . "\n";
|
||||||
|
foreach ($cols as $c) {
|
||||||
|
echo ($c['Field'] ?? '') . "\n";
|
||||||
|
}
|
||||||
|
$data = $mdb->query("SELECT id, name, active, COALESCE(google_ads_customer_id,'') AS google_ads_customer_id FROM clients ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
echo "ROWS=" . count($data) . "\n";
|
||||||
|
foreach ($data as $r) {
|
||||||
|
echo json_encode($r, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
}
|
||||||
|
$eligible = $mdb->query("SELECT id FROM clients WHERE COALESCE(deleted,0)=0 AND active=1 AND google_ads_customer_id IS NOT NULL AND google_ads_customer_id<>'' ORDER BY id ASC")->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
echo "ELIGIBLE=" . json_encode($eligible) . "\n";
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo "ERR: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
?>
|
||||||
35
tmp/debug_clients_remote.php
Normal file
35
tmp/debug_clients_remote.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', '1');
|
||||||
|
require 'config.php';
|
||||||
|
require 'libraries/medoo/medoo.php';
|
||||||
|
|
||||||
|
$host = $database['remote_host'];
|
||||||
|
try {
|
||||||
|
$mdb = new medoo([
|
||||||
|
'database_type' => 'mysql',
|
||||||
|
'database_name' => $database['name'],
|
||||||
|
'server' => $host,
|
||||||
|
'username' => $database['user'],
|
||||||
|
'password' => $database['password'],
|
||||||
|
'charset' => 'utf8'
|
||||||
|
]);
|
||||||
|
|
||||||
|
echo "CONNECTED_REMOTE=" . $host . PHP_EOL;
|
||||||
|
|
||||||
|
$rows = $mdb->query("SELECT id, name, COALESCE(active,0) AS active, COALESCE(deleted,0) AS deleted, CONCAT('[', COALESCE(google_ads_customer_id,''), ']') AS google_ads_customer_id FROM clients ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
echo "CLIENTS_TOTAL=" . count($rows) . PHP_EOL;
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
echo json_encode($r, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$eligible = $mdb->query("SELECT id, name FROM clients WHERE COALESCE(deleted,0)=0 AND COALESCE(active,0)=1 AND TRIM(COALESCE(google_ads_customer_id,''))<>'' ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
echo "ELIGIBLE_TOTAL=" . count($eligible) . PHP_EOL;
|
||||||
|
foreach ($eligible as $e) {
|
||||||
|
echo "ELIGIBLE=" . json_encode($e, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
echo "ERR=" . $e->getMessage() . PHP_EOL;
|
||||||
|
}
|
||||||
|
?>
|
||||||
20
tmp/debug_clients_remote2.php
Normal file
20
tmp/debug_clients_remote2.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
require 'config.php';
|
||||||
|
require 'libraries/medoo/medoo.php';
|
||||||
|
$mdb = new medoo([
|
||||||
|
'database_type' => 'mysql',
|
||||||
|
'database_name' => $database['name'],
|
||||||
|
'server' => $database['remote_host'],
|
||||||
|
'username' => $database['user'],
|
||||||
|
'password' => $database['password'],
|
||||||
|
'charset' => 'utf8'
|
||||||
|
]);
|
||||||
|
$cols = $mdb->query("SHOW COLUMNS FROM clients")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
echo "COLUMNS:" . PHP_EOL;
|
||||||
|
foreach ($cols as $c) { echo $c['Field'] . PHP_EOL; }
|
||||||
|
$rows = $mdb->query("SELECT id, name, COALESCE(active,0) AS active, CONCAT('[', COALESCE(google_ads_customer_id,''), ']') AS google_ads_customer_id FROM clients ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
echo "ROWS=" . count($rows) . PHP_EOL;
|
||||||
|
foreach ($rows as $r) { echo json_encode($r, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . PHP_EOL; }
|
||||||
|
$eligible = $mdb->query("SELECT id, name FROM clients WHERE COALESCE(active,0)=1 AND TRIM(COALESCE(google_ads_customer_id,''))<>'' ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
echo "ELIGIBLE=" . json_encode($eligible, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||||
|
?>
|
||||||
14
tmp/debug_eligible_remote.php
Normal file
14
tmp/debug_eligible_remote.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
require 'config.php';
|
||||||
|
require 'libraries/medoo/medoo.php';
|
||||||
|
$mdb = new medoo([
|
||||||
|
'database_type' => 'mysql',
|
||||||
|
'database_name' => $database['name'],
|
||||||
|
'server' => $database['remote_host'],
|
||||||
|
'username' => $database['user'],
|
||||||
|
'password' => $database['password'],
|
||||||
|
'charset' => 'utf8'
|
||||||
|
]);
|
||||||
|
$rows = $mdb->query("SELECT id, name FROM clients WHERE COALESCE(active,0)=1 AND TRIM(COALESCE(google_ads_customer_id,''))<>'' ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
echo json_encode($rows, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||||
|
?>
|
||||||
Reference in New Issue
Block a user