Add keyword status toggle functionality and styling

- Introduced a new button to toggle the status of keywords between PAUSED and ENABLED in the keywords table.
- Added corresponding styles for the toggle button to enhance user experience.
- Updated the keywords table rendering logic to display the status and toggle button correctly.
- Created a new migration to add a 'status' column to the 'campaign_keywords' table, defaulting to 'ENABLED'.
This commit is contained in:
2026-02-24 23:31:17 +01:00
parent 2a87d0b77e
commit 651d925b20
10 changed files with 324 additions and 24 deletions

View File

@@ -219,8 +219,8 @@
},
"class.GoogleAdsApi.php": {
"type": "-",
"size": 116731,
"lmtime": 1771851152230,
"size": 122118,
"lmtime": 1771954979121,
"modified": false
},
"class.OpenAiApi.php": {
@@ -272,9 +272,9 @@
"docs": {
"database.sql": {
"type": "-",
"size": 17320,
"size": 9123,
"lmtime": 1771440593718,
"modified": false
"modified": true
},
"google_ads_api_design_doc.doc": {
"type": "-",
@@ -290,14 +290,14 @@
},
"memory.md": {
"type": "-",
"size": 3357,
"size": 21742,
"lmtime": 1771496247126,
"modified": false
"modified": true
},
"PLAN.md": {
"class-methods.md": {
"type": "-",
"size": 11544,
"lmtime": 0,
"size": 58706,
"lmtime": 1771954009821,
"modified": false
}
},
@@ -334,14 +334,14 @@
},
"style.css": {
"type": "-",
"size": 57679,
"lmtime": 1771757366015,
"size": 58603,
"lmtime": 1771955179296,
"modified": false
},
"style.css.map": {
"type": "-",
"size": 154096,
"lmtime": 1771757366015,
"size": 156429,
"lmtime": 1771955179296,
"modified": false
},
"style-old.css": {
@@ -358,8 +358,8 @@
},
"style.scss": {
"type": "-",
"size": 67231,
"lmtime": 1771757365508,
"size": 68201,
"lmtime": 1771955178718,
"modified": false
}
},
@@ -652,8 +652,8 @@
"campaign_terms": {
"main_view.php": {
"type": "-",
"size": 81511,
"lmtime": 1771717410322,
"size": 94858,
"lmtime": 1771954474677,
"modified": false
}
},

View File

@@ -942,4 +942,86 @@ class CampaignTerms
] ) );
exit;
}
/**
* Wstrzymuje lub wznawia fraze kluczowa (ENABLED <-> PAUSED).
*/
static public function toggle_keyword_status()
{
$keyword_id = (int) \S::get( 'keyword_id' );
if ( $keyword_id <= 0 )
{
echo json_encode( [ 'success' => false, 'message' => 'Nie podano frazy.' ] );
exit;
}
$context = \factory\Campaigns::get_keyword_context( $keyword_id );
if ( !$context )
{
echo json_encode( [ 'success' => false, 'message' => 'Nie znaleziono danych frazy.' ] );
exit;
}
$customer_id = trim( (string) ( $context['google_ads_customer_id'] ?? '' ) );
$ad_group_external_id = trim( (string) ( $context['external_ad_group_id'] ?? '' ) );
$keyword_text = trim( (string) ( $context['keyword_text'] ?? '' ) );
$match_type = strtoupper( trim( (string) ( $context['match_type'] ?? 'BROAD' ) ) );
$current_status = strtoupper( trim( (string) ( $context['status'] ?? 'ENABLED' ) ) );
if ( $customer_id === '' || $ad_group_external_id === '' || $keyword_text === '' )
{
echo json_encode( self::with_optional_debug( [
'success' => false,
'message' => 'Brak wymaganych danych Google Ads dla tej frazy.',
], [ 'context' => $context ] ) );
exit;
}
$new_status = ( $current_status === 'PAUSED' ) ? 'ENABLED' : 'PAUSED';
$api = new \services\GoogleAdsApi();
if ( !$api -> is_configured() )
{
echo json_encode( [ 'success' => false, 'message' => 'Google Ads API nie jest skonfigurowane.' ] );
exit;
}
$api_result = $api -> update_keyword_status( $customer_id, $ad_group_external_id, $keyword_text, $match_type, $new_status );
if ( !( $api_result['success'] ?? false ) )
{
$last_error = \services\GoogleAdsApi::get_setting( 'google_ads_last_error' );
echo json_encode( self::with_optional_debug( [
'success' => false,
'message' => 'Nie udalo sie zmienic statusu frazy w Google Ads.',
'error' => $last_error
], [
'customer_id' => $customer_id,
'ad_group_external_id' => $ad_group_external_id,
'keyword_text' => $keyword_text,
'match_type' => $match_type,
'new_status' => $new_status,
'api_result' => $api_result
] ) );
exit;
}
\factory\Campaigns::update_campaign_keyword_status( $keyword_id, $new_status );
$status_label = ( $new_status === 'PAUSED' ) ? 'wstrzymana' : 'wznowiona';
echo json_encode( self::with_optional_debug( [
'success' => true,
'message' => 'Fraza zostala ' . $status_label . '.',
'new_status' => $new_status
], [
'customer_id' => $customer_id,
'ad_group_external_id' => $ad_group_external_id,
'keyword_text' => $keyword_text,
'match_type' => $match_type,
'api_result' => $api_result
] ) );
exit;
}
}

View File

@@ -4033,6 +4033,7 @@ class Cron
}
$match_type = trim( (string) ( $row_30['match_type'] ?? ( $row_all_time['match_type'] ?? '' ) ) );
$status = trim( (string) ( $row_all_time['status'] ?? ( $row_30['status'] ?? 'ENABLED' ) ) );
$clicks_30 = (int) ( $row_30['clicks'] ?? 0 );
$clicks_all_time = (int) ( $row_all_time['clicks'] ?? 0 );
@@ -4046,6 +4047,7 @@ class Cron
'ad_group_id' => $db_ad_group_id,
'keyword_text' => $keyword_text,
'match_type' => $match_type,
'status' => $status,
'impressions_30' => (int) ( $row_30['impressions'] ?? 0 ),
'clicks_30' => $clicks_30,
'cost_30' => (float) ( $row_30['cost'] ?? 0 ),

View File

@@ -183,6 +183,7 @@ class Campaigns
ag.ad_group_name,
kw.keyword_text,
kw.match_type,
kw.status,
kw.impressions_30,
kw.clicks_30,
kw.cost_30,
@@ -328,6 +329,7 @@ class Campaigns
kw.id AS keyword_row_id,
kw.keyword_text,
kw.match_type,
kw.status,
kw.campaign_id AS db_campaign_id,
kw.ad_group_id AS db_ad_group_id,
c.client_id,
@@ -373,6 +375,12 @@ class Campaigns
return $mdb -> delete( 'campaign_keywords', [ 'id' => (int) $keyword_id ] );
}
static public function update_campaign_keyword_status( $keyword_id, $status )
{
global $mdb;
return $mdb -> update( 'campaign_keywords', [ 'status' => $status ], [ 'id' => (int) $keyword_id ] );
}
static public function delete_campaign( $campaign_id )
{
global $mdb;

View File

@@ -2143,6 +2143,92 @@ class GoogleAdsApi
];
}
/**
* Zmienia status keywordu (ENABLED / PAUSED) w Google Ads.
*/
public function update_keyword_status( $customer_id, $ad_group_id, $keyword_text, $match_type, $new_status )
{
$customer_id = trim( str_replace( '-', '', (string) $customer_id ) );
$ad_group_id = trim( (string) $ad_group_id );
$keyword_text = trim( (string) $keyword_text );
$match_type = strtoupper( trim( (string) $match_type ) );
$new_status = strtoupper( trim( (string) $new_status ) );
if ( $customer_id === '' || $ad_group_id === '' || $keyword_text === '' )
{
self::set_setting( 'google_ads_last_error', 'Brak wymaganych danych do zmiany statusu frazy.' );
return [ 'success' => false ];
}
if ( !in_array( $new_status, [ 'ENABLED', 'PAUSED' ], true ) )
{
self::set_setting( 'google_ads_last_error', 'Nieprawidlowy status: ' . $new_status );
return [ 'success' => false ];
}
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
{
$match_type = 'BROAD';
}
$keyword_text_escaped = $this -> gaql_escape( $keyword_text );
$gaql = "SELECT "
. "ad_group_criterion.resource_name "
. "FROM ad_group_criterion "
. "WHERE ad_group.id = " . $ad_group_id . " "
. "AND ad_group_criterion.type = 'KEYWORD' "
. "AND ad_group_criterion.negative = FALSE "
. "AND ad_group_criterion.keyword.text = '" . $keyword_text_escaped . "' "
. "AND ad_group_criterion.keyword.match_type = " . $match_type . " "
. "LIMIT 1";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false )
{
return [ 'success' => false ];
}
$resource_name = '';
foreach ( (array) $results as $row )
{
$resource_name = (string) ( $row['adGroupCriterion']['resourceName'] ?? '' );
if ( $resource_name === '' )
{
$resource_name = (string) ( $row['ad_group_criterion']['resource_name'] ?? '' );
}
if ( $resource_name !== '' ) break;
}
if ( $resource_name === '' )
{
self::set_setting( 'google_ads_last_error', 'Nie znaleziono frazy w Google Ads.' );
return [ 'success' => false, 'not_found' => true ];
}
$operation = [
'adGroupCriterionOperation' => [
'updateMask' => 'status',
'update' => [
'resourceName' => $resource_name,
'status' => $new_status
]
]
];
$mutate_result = $this -> mutate( $customer_id, [ $operation ] );
if ( $mutate_result === false )
{
return [ 'success' => false, 'sent_operation' => $operation ];
}
return [
'success' => true,
'new_status' => $new_status,
'response' => $mutate_result,
'sent_operation' => $operation
];
}
private function verify_negative_keyword_exists( $customer_id, $scope, $keyword_text, $match_type, $campaign_id = null, $ad_group_id = null )
{
$customer_id = trim( str_replace( '-', '', (string) $customer_id ) );
@@ -3221,6 +3307,7 @@ class GoogleAdsApi
. "ad_group.id, "
. "ad_group_criterion.keyword.text, "
. "ad_group_criterion.keyword.match_type, "
. "ad_group_criterion.status, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
@@ -3230,7 +3317,7 @@ class GoogleAdsApi
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "AND ad_group_criterion.negative = FALSE "
. "AND ad_group_criterion.status = 'ENABLED' "
. "AND ad_group_criterion.status != 'REMOVED' "
. "AND campaign.advertising_channel_type = 'SEARCH' "
. "AND metrics.clicks > 0 "
. "AND segments.date DURING LAST_30_DAYS";
@@ -3248,6 +3335,7 @@ class GoogleAdsApi
. "ad_group.id, "
. "ad_group_criterion.keyword.text, "
. "ad_group_criterion.keyword.match_type, "
. "ad_group_criterion.status, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
@@ -3257,7 +3345,7 @@ class GoogleAdsApi
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "AND ad_group_criterion.negative = FALSE "
. "AND ad_group_criterion.status = 'ENABLED' "
. "AND ad_group_criterion.status != 'REMOVED' "
. "AND campaign.advertising_channel_type = 'SEARCH' "
. "AND metrics.clicks > 0";
@@ -3626,6 +3714,7 @@ class GoogleAdsApi
$ad_group_id = $row['adGroup']['id'] ?? null;
$keyword_text = trim( (string) ( $row['adGroupCriterion']['keyword']['text'] ?? '' ) );
$match_type = trim( (string) ( $row['adGroupCriterion']['keyword']['matchType'] ?? '' ) );
$status = trim( (string) ( $row['adGroupCriterion']['status'] ?? 'ENABLED' ) );
if ( !$campaign_id || !$ad_group_id || $keyword_text === '' )
{
@@ -3641,6 +3730,7 @@ class GoogleAdsApi
'ad_group_id' => (int) $ad_group_id,
'keyword_text' => $keyword_text,
'match_type' => $match_type,
'status' => $status,
'impressions' => 0,
'clicks' => 0,
'cost' => 0.0,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3186,6 +3186,38 @@ table#products {
}
}
.terms-toggle-keyword-status-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #FDE68A;
background: #FFFBEB;
color: #D97706;
&:hover {
background: #D97706;
color: #FFFFFF;
border-color: #D97706;
}
&.text-success {
border-color: #A7F3D0;
background: #ECFDF5;
color: #059669;
&:hover {
background: #059669;
color: #FFFFFF;
border-color: #059669;
}
}
}
.terms-change-match-type-btn {
display: inline-flex;
align-items: center;

View File

@@ -0,0 +1,2 @@
ALTER TABLE `campaign_keywords`
ADD COLUMN `status` varchar(20) NOT NULL DEFAULT 'ENABLED' AFTER `match_type`;

View File

@@ -1612,12 +1612,22 @@ function build_keywords_table( rows )
pagingType: 'simple_numbers',
order: [[ 4, 'desc' ]],
columns: [
{ data: 'keyword_text', defaultContent: '' },
{ data: 'keyword_text', defaultContent: '', render: function( data, type, row ) {
if ( type !== 'display' ) return data;
var text = $('<span>').text( data ).html();
if ( row.status === 'PAUSED' ) return '<span class="text-muted">' + text + '</span>';
return text;
} },
{ data: 'match_type', defaultContent: '', render: render_match_type_label },
{ data: 'ad_group_name', defaultContent: '-' },
{ data: 'id', orderable: false, searchable: false, render: function( data, type, row ) {
if ( type !== 'display' ) return data;
return '<button type="button" class="terms-change-match-type-btn" data-keyword-id="' + data + '" title="Zmien dopasowanie"><i class="fa-solid fa-pen-to-square"></i></button>' +
var is_paused = ( row.status === 'PAUSED' );
var toggle_icon = is_paused ? 'fa-play' : 'fa-pause';
var toggle_title = is_paused ? 'Wznow fraze' : 'Wstrzymaj fraze';
var toggle_class = is_paused ? 'text-success' : 'text-warning';
return '<button type="button" class="terms-toggle-keyword-status-btn ' + toggle_class + '" data-keyword-id="' + data + '" title="' + toggle_title + '"><i class="fa-solid ' + toggle_icon + '"></i></button>' +
' <button type="button" class="terms-change-match-type-btn" data-keyword-id="' + data + '" title="Zmien dopasowanie"><i class="fa-solid fa-pen-to-square"></i></button>' +
' <button type="button" class="terms-delete-keyword-btn" data-keyword-id="' + data + '" title="Usun fraze"><i class="fa-solid fa-trash"></i></button>';
} },
{ data: 'clicks_all_time', render: function( data, type ){ return type === 'display' ? format_num( data, 0 ) : Number( data || 0 ); } },
@@ -1637,7 +1647,7 @@ function build_keywords_table( rows )
columnDefs: [
{ targets: 0, className: 'text-cell phrase-nowrap', width: '220px' },
{ targets: [1,2], className: 'text-cell', width: '140px' },
{ targets: 3, className: 'dt-center', width: '90px' },
{ targets: 3, className: 'dt-center', width: '110px' },
{ targets: [4,9], className: 'num-cell', width: '70px' },
{ targets: [5,6,10,11], className: 'num-cell', width: '85px' },
{ targets: [7,12], className: 'num-cell', width: '80px' },
@@ -2523,6 +2533,80 @@ $( function()
});
});
$( 'body' ).on( 'click', '.terms-toggle-keyword-status-btn', function()
{
var btn = $( this );
var keyword_id = parseInt( btn.data( 'keyword-id' ), 10 );
var row_data = terms_keywords_table ? terms_keywords_table.row( btn.closest( 'tr' ) ).data() : null;
var keyword_text = row_data ? String( row_data.keyword_text || '' ) : '';
var is_paused = row_data && row_data.status === 'PAUSED';
if ( !keyword_id ) return;
var action_label = is_paused ? 'wznowic' : 'wstrzymac';
var action_past = is_paused ? 'wznowiona' : 'wstrzymana';
$.confirm({
title: is_paused ? 'Wznow fraze' : 'Wstrzymaj fraze',
columnClass: 'col-md-4 col-md-offset-4',
content: 'Czy na pewno ' + action_label + ' fraze <strong>' + $('<span>').text( keyword_text ).html() + '</strong>?',
type: is_paused ? 'green' : 'orange',
buttons: {
confirm: {
text: is_paused ? 'Wznow' : 'Wstrzymaj',
btnClass: is_paused ? 'btn-green' : 'btn-warning',
keys: [ 'enter' ],
action: function()
{
var modal = this;
modal.showLoading( true );
$.ajax({
url: '/campaign_terms/toggle_keyword_status/',
type: 'POST',
data: { keyword_id: keyword_id },
success: function( response )
{
var data = JSON.parse( response );
modal.close();
var debugHtml = terms_build_debug_details_html( data.debug || null );
if ( data.success )
{
var successDialog = $.alert({
title: 'Sukces',
columnClass: 'col-md-4 col-md-offset-4',
content: ( data.message || 'Status frazy zmieniony.' ) + debugHtml,
type: 'green'
});
setTimeout( function() { successDialog.close(); }, 5000 );
load_phrase_tables();
}
else
{
$.alert({
title: 'Blad',
columnClass: 'col-md-4 col-md-offset-4',
content: ( data.message || 'Nie udalo sie zmienic statusu.' ) + ( data.error ? '<br><small>' + data.error + '</small>' : '' ) + debugHtml,
type: 'red'
});
}
},
error: function()
{
modal.close();
$.alert({ title: 'Blad', columnClass: 'col-md-4 col-md-offset-4', content: 'Blad komunikacji z serwerem.', type: 'red' });
}
});
return false;
}
},
cancel: { text: 'Anuluj' }
}
});
});
$( 'body' ).on( 'click', '#terms_add_keyword_btn', function()
{
var campaign_id = $( '#terms_campaign_id' ).val();