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:
34
.vscode/ftp-kr.sync.cache.json
vendored
34
.vscode/ftp-kr.sync.cache.json
vendored
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
2
migrations/025_campaign_keywords_status.sql
Normal file
2
migrations/025_campaign_keywords_status.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `campaign_keywords`
|
||||
ADD COLUMN `status` varchar(20) NOT NULL DEFAULT 'ENABLED' AFTER `match_type`;
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user