feat: Add campaign comments functionality with API support and database migration

This commit is contained in:
2026-03-06 20:07:11 +01:00
parent 5524324bea
commit 5dc2a89748
6 changed files with 244 additions and 26 deletions

View File

@@ -9,8 +9,8 @@
},
"api.php": {
"type": "-",
"size": 2446,
"lmtime": 0,
"size": 4040,
"lmtime": 1772671706766,
"modified": false
},
"autoload": {
@@ -71,9 +71,9 @@
},
"class.Api.php": {
"type": "-",
"size": 20317,
"lmtime": 1744498273470,
"modified": true
"size": 25694,
"lmtime": 1772667215646,
"modified": false
},
"class.CampaignAlerts.php": {
"type": "-",
@@ -83,8 +83,8 @@
},
"class.Campaigns.php": {
"type": "-",
"size": 6153,
"lmtime": 1771626580811,
"size": 7262,
"lmtime": 1772671343928,
"modified": false
},
"class.CampaignTerms.php": {
@@ -157,8 +157,8 @@
},
"class.Campaigns.php": {
"type": "-",
"size": 13229,
"lmtime": 1771966790175,
"size": 14333,
"lmtime": 1772671326827,
"modified": false
},
"class.Clients.php": {
@@ -707,6 +707,12 @@
"lmtime": 1771966738564,
"modified": false
},
"026_clients_bestseller_settings.sql": {
"type": "-",
"size": 1613,
"lmtime": 1772611513545,
"modified": false
},
"026_cron_queue.sql": {
"type": "-",
"size": 1851,
@@ -719,10 +725,10 @@
"lmtime": 0,
"modified": true
},
"026_clients_bestseller_settings.sql": {
"027_campaigns_comments.sql": {
"type": "-",
"size": 1613,
"lmtime": 1772611513545,
"size": 482,
"lmtime": 1772671313573,
"modified": false
}
},
@@ -738,8 +744,8 @@
"campaigns": {
"main_view.php": {
"type": "-",
"size": 21607,
"lmtime": 1771717394441,
"size": 24191,
"lmtime": 1772672347271,
"modified": false
}
},

98
api.php
View File

@@ -71,6 +71,104 @@ if ( \S::get( 'action' ) == 'domain_opr_check' )
exit;
}
// Dodawanie komentarza do kampanii przez API (z Claude Code)
if ( \S::get( 'action' ) == 'campaign_comment_add' )
{
$api_key = trim( \S::get( 'api_key' ) );
$stored_key = $mdb -> get( 'settings', 'setting_value', [ 'setting_key' => 'api_key' ] );
if ( !$api_key || !$stored_key || $api_key !== $stored_key )
{
echo json_encode( [ 'result' => 'error', 'message' => 'Invalid api_key' ] );
exit;
}
$external_campaign_id = trim( \S::get( 'campaign_id' ) );
$client_id_param = trim( \S::get( 'client_id' ) );
$comment = trim( \S::get( 'comment' ) );
$date = \S::get( 'date' ) ?: date( 'Y-m-d' );
if ( !$external_campaign_id || !$client_id_param || !$comment )
{
echo json_encode( [ 'result' => 'error', 'message' => 'Missing required params: campaign_id, client_id, comment' ] );
exit;
}
$client_id_clean = str_replace( '-', '', $client_id_param );
$local_campaign = $mdb -> query(
'SELECT c.id
FROM campaigns c
JOIN clients cl ON c.client_id = cl.id
WHERE c.campaign_id = :campaign_id
AND REPLACE( cl.google_ads_customer_id, \'-\', \'\' ) = :client_id
LIMIT 1',
[
':campaign_id' => $external_campaign_id,
':client_id' => $client_id_clean
]
) -> fetch( \PDO::FETCH_ASSOC );
if ( !$local_campaign )
{
echo json_encode( [ 'result' => 'error', 'message' => 'Campaign not found' ] );
exit;
}
\factory\Campaigns::add_campaign_comment( $local_campaign['id'], $comment, $date );
echo json_encode( [ 'result' => 'ok' ] );
exit;
}
// Zmiana custom_label_4 dla produktu przez API
if ( \S::get( 'action' ) == 'product_custom_label_4_set' )
{
$api_key = trim( \S::get( 'api_key' ) );
$stored_key = $mdb -> get( 'settings', 'setting_value', [ 'setting_key' => 'api_key' ] );
if ( !$api_key || !$stored_key || $api_key !== $stored_key )
{
echo json_encode( [ 'result' => 'error', 'message' => 'Invalid api_key' ] );
exit;
}
$offer_id = trim( \S::get( 'offer_id' ) );
$client_id_param = trim( \S::get( 'client_id' ) );
$custom_label_4 = trim( \S::get( 'custom_label_4' ) );
if ( !$offer_id || !$client_id_param )
{
echo json_encode( [ 'result' => 'error', 'message' => 'Missing required params: offer_id, client_id' ] );
exit;
}
$product = $mdb -> query(
'SELECT p.id
FROM products p
JOIN clients cl ON p.client_id = cl.id
WHERE p.offer_id = :offer_id
AND cl.id = :client_id
LIMIT 1',
[
':offer_id' => $offer_id,
':client_id' => (int) $client_id_param
]
) -> fetch( \PDO::FETCH_ASSOC );
if ( !$product )
{
echo json_encode( [ 'result' => 'error', 'message' => 'Product not found' ] );
exit;
}
\factory\Products::set_product_data( $product['id'], 'custom_label_4', $custom_label_4 );
\factory\Products::add_product_comment( $product['id'], 'Zmiana etykiety 4 na: ' . $custom_label_4 . ' (API)' );
echo json_encode( [ 'result' => 'ok' ] );
exit;
}
// Open Page Rank - zapis
if ( \S::get( 'action' ) == 'domain_opr_save' )
{

View File

@@ -103,7 +103,7 @@ class Campaigns
echo json_encode( [
'chart_data' => $chart_data,
'dates' => $dates,
'comments' => []
'comments' => \factory\Campaigns::get_campaign_comments( $campaign_id )
] );
exit;
}
@@ -221,4 +221,38 @@ class Campaigns
echo json_encode( [ 'success' => true, 'deleted' => $deleted ] );
exit;
}
static public function comment_add()
{
$campaign_id = (int) \S::get( 'campaign_id' );
$comment = trim( \S::get( 'comment' ) );
$date = \S::get( 'date' );
if ( !$campaign_id || !$comment )
{
echo json_encode( [ 'success' => false, 'message' => 'Brak wymaganych parametrów' ] );
exit;
}
$result = \factory\Campaigns::add_campaign_comment( $campaign_id, $comment, $date ?: null );
echo json_encode( [ 'success' => $result ? true : false ] );
exit;
}
static public function comment_delete()
{
$comment_id = (int) \S::get( 'comment_id' );
if ( !$comment_id )
{
echo json_encode( [ 'success' => false, 'message' => 'Nie podano komentarza' ] );
exit;
}
$result = \factory\Campaigns::delete_campaign_comment( $comment_id );
echo json_encode( [ 'success' => $result ? true : false ] );
exit;
}
}

View File

@@ -415,4 +415,31 @@ class Campaigns
$mdb -> delete( 'campaigns_history', [ 'id' => $ids ] );
return count( $ids );
}
static public function get_campaign_comments( $campaign_id )
{
global $mdb;
return $mdb -> query( 'SELECT id, comment, date_add FROM campaigns_comments WHERE campaign_id = \'' . (int) $campaign_id . '\' ORDER BY date_add DESC' ) -> fetchAll( \PDO::FETCH_ASSOC );
}
static public function add_campaign_comment( $campaign_id, $comment, $date = null )
{
global $mdb;
if ( !$date )
$date = date( 'Y-m-d' );
else
$date = date( 'Y-m-d', strtotime( $date ) );
if ( $mdb -> count( 'campaigns_comments', [ 'AND' => [ 'campaign_id' => $campaign_id, 'date_add' => $date ] ] ) )
return $mdb -> update( 'campaigns_comments', [ 'comment' => $comment ], [ 'AND' => [ 'campaign_id' => $campaign_id, 'date_add' => $date ] ] );
else
return $mdb -> insert( 'campaigns_comments', [ 'campaign_id' => $campaign_id, 'comment' => $comment, 'date_add' => $date ] );
}
static public function delete_campaign_comment( $comment_id )
{
global $mdb;
return $mdb -> delete( 'campaigns_comments', [ 'id' => $comment_id ] );
}
}

View File

@@ -0,0 +1,10 @@
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,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_campaign_comment_date` (`campaign_id`, `date_add`),
KEY `idx_campaign_id` (`campaign_id`),
CONSTRAINT `FK_campaigns_comments_campaigns` FOREIGN KEY (`campaign_id`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View File

@@ -235,23 +235,25 @@ function reloadChart()
{
var parsedData = JSON.parse( response );
var plotLines = [];
var chartComments = parsedData.comments || [];
var commentData = [];
chartComments.forEach( function( comment ) {
var idx = parsedData.dates.indexOf( comment.date_add.split(' ')[0] );
if ( idx < 0 ) return;
commentData.push({ idx: idx, text: comment.comment, date: comment.date_add });
parsedData.comments.forEach( function( comment ) {
plotLines.push({
color: '#333333',
width: 1,
value: parsedData.dates.indexOf( comment.date_add.split(' ')[0] ),
dashStyle: 'Solid',
label: {
text: comment.comment,
align: 'left',
style: { color: '#333333', fontSize: '13px' }
},
color: '#FF6B00',
width: 2,
value: idx,
dashStyle: 'Dash',
zIndex: 5
});
});
Highcharts.chart( 'container', {
var chart = Highcharts.chart( 'container', {
chart: {
style: { fontFamily: '"Roboto", sans-serif' },
backgroundColor: 'transparent'
@@ -296,6 +298,47 @@ function reloadChart()
tooltip: { style: { fontSize: '13px' } },
credits: { enabled: false }
});
// Nakładamy klikalne ikonki komentarzy jako elementy HTML nad wykresem
$( '#container .chart-comment-icons' ).remove();
if ( chart && commentData.length ) {
var $wrap = $( '<div class="chart-comment-icons"></div>' ).css({ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, 'pointer-events': 'none', 'z-index': 10, overflow: 'visible' });
$( '#container' ).css({ position: 'relative', overflow: 'visible' }).append( $wrap );
commentData.forEach( function( c, i ) {
var px = chart.xAxis[0].toPixels( c.idx, false );
var $icon = $( '<i class="fa-solid fa-comment"></i>' )
.css({
position: 'absolute',
left: px - 8,
top: chart.plotTop + 5,
color: '#FF6B00',
fontSize: '16px',
cursor: 'pointer',
'pointer-events': 'auto'
})
.attr( 'title', 'Komentarz \u2014 ' + c.date )
.data( 'comment-index', i );
$wrap.append( $icon );
});
$wrap.on( 'click', 'i', function() {
var c = commentData[ $( this ).data( 'comment-index' ) ];
if ( !c ) return;
var html = $( '<span>' ).text( c.text ).html();
html = html.replace( /\n/g, '<br>' );
html = html.replace( /\. /g, '.<br>' );
$.alert({
title: '<i class="fa-solid fa-comment" style="color:#FF6B00;"></i> Komentarz \u2014 ' + c.date,
content: '<div style="font-size:14px;line-height:1.7;">' + html + '</div>',
closeIcon: true,
columnClass: 'col-md-6 col-md-offset-3',
buttons: { ok: { text: 'Zamknij', btnClass: 'btn-blue' } }
});
});
}
},
error: function( jqXHR, textStatus, errorThrown ) {
console.error( 'Error AJAX:', textStatus, errorThrown );