diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index 9a1c73c..9632579 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -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 } }, diff --git a/api.php b/api.php index 0828fa4..b2099f2 100644 --- a/api.php +++ b/api.php @@ -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' ) { diff --git a/autoload/controls/class.Campaigns.php b/autoload/controls/class.Campaigns.php index 0e229dc..405bbe2 100644 --- a/autoload/controls/class.Campaigns.php +++ b/autoload/controls/class.Campaigns.php @@ -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; + } } diff --git a/autoload/factory/class.Campaigns.php b/autoload/factory/class.Campaigns.php index 498314f..e4741b7 100644 --- a/autoload/factory/class.Campaigns.php +++ b/autoload/factory/class.Campaigns.php @@ -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 ] ); + } } diff --git a/migrations/027_campaigns_comments.sql b/migrations/027_campaigns_comments.sql new file mode 100644 index 0000000..2669e69 --- /dev/null +++ b/migrations/027_campaigns_comments.sql @@ -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; diff --git a/templates/campaigns/main_view.php b/templates/campaigns/main_view.php index ab67544..f7a5888 100644 --- a/templates/campaigns/main_view.php +++ b/templates/campaigns/main_view.php @@ -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 = $( '
' ).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 = $( '' ) + .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 = $( '' ).text( c.text ).html(); + html = html.replace( /\n/g, '