- Changed font from Open Sans to Roboto in layout files. - Added campaign and ad group filters in products main view. - Enhanced product history to include campaign and ad group IDs. - Updated migrations to support new campaign and ad group dimensions in product statistics. - Introduced new migration files for managing campaign types and dropping obsolete columns.
547 lines
19 KiB
PHP
547 lines
19 KiB
PHP
<?php
|
||
namespace controls;
|
||
class Products
|
||
{
|
||
static public function get_client_bestseller_min_roas() {
|
||
$client_id = \S::get( 'client_id' );
|
||
|
||
$min_roas = \factory\Products::get_client_bestseller_min_roas( $client_id );
|
||
|
||
if ( $min_roas )
|
||
{
|
||
echo json_encode( [ 'status' => 'ok', 'min_roas' => $min_roas ] );
|
||
}
|
||
else
|
||
echo json_encode( [ 'status' => 'error' ] );
|
||
exit;
|
||
}
|
||
|
||
static public function save_client_bestseller_min_roas() {
|
||
$client_id = \S::get( 'client_id' );
|
||
$min_roas = \S::get( 'min_roas' );
|
||
|
||
if ( \factory\Products::save_client_bestseller_min_roas( $client_id, $min_roas ) )
|
||
{
|
||
echo json_encode( [ 'status' => 'ok' ] );
|
||
}
|
||
else
|
||
echo json_encode( [ 'status' => 'error' ] );
|
||
exit;
|
||
}
|
||
|
||
static public function main_view()
|
||
{
|
||
return \Tpl::view( 'products/main_view', [
|
||
'clients' => \factory\Campaigns::get_clients(),
|
||
] );
|
||
}
|
||
|
||
static public function get_campaigns_list()
|
||
{
|
||
$client_id = (int) \S::get( 'client_id' );
|
||
echo json_encode( [ 'campaigns' => \factory\Campaigns::get_campaigns_list( $client_id, true ) ] );
|
||
exit;
|
||
}
|
||
|
||
static public function get_campaign_ad_groups()
|
||
{
|
||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||
|
||
if ( $campaign_id <= 0 )
|
||
{
|
||
echo json_encode( [ 'ad_groups' => [] ] );
|
||
exit;
|
||
}
|
||
|
||
echo json_encode( [ 'ad_groups' => \factory\Campaigns::get_campaign_ad_groups( $campaign_id ) ] );
|
||
exit;
|
||
}
|
||
|
||
static public function comment_add()
|
||
{
|
||
$product_id = \S::get( 'product_id' );
|
||
$date = \S::get( 'date' );
|
||
$comment = \S::get( 'comment' );
|
||
|
||
if ( \factory\Products::add_product_comment( $product_id, $comment, $date ) )
|
||
{
|
||
echo json_encode( [ 'status' => 'ok' ] );
|
||
}
|
||
else
|
||
echo json_encode( [ 'status' => 'error' ] );
|
||
exit;
|
||
}
|
||
|
||
static public function comment_delete()
|
||
{
|
||
$comment_id = \S::get( 'comment_id' );
|
||
|
||
if ( \factory\Products::delete_product_comment( $comment_id ) )
|
||
{
|
||
echo json_encode( [ 'status' => 'ok' ] );
|
||
}
|
||
else
|
||
echo json_encode( [ 'status' => 'error' ] );
|
||
exit;
|
||
}
|
||
|
||
static public function get_product_data() {
|
||
$product_id = \S::get( 'product_id' );
|
||
|
||
$product_title = \factory\Products::get_product_data( $product_id, 'title' );
|
||
$product_description = \factory\Products::get_product_data( $product_id, 'description' );
|
||
$google_product_category = \factory\Products::get_product_data( $product_id, 'google_product_category' );
|
||
$product_url = \factory\Products::get_product_data( $product_id, 'product_url' );
|
||
|
||
echo json_encode( [ 'status' => 'ok', 'product_details' => [
|
||
'title' => $product_title,
|
||
'description' => $product_description,
|
||
'google_product_category' => $google_product_category,
|
||
'product_url' => $product_url
|
||
] ] );
|
||
exit;
|
||
}
|
||
|
||
static public function ai_suggest()
|
||
{
|
||
$product_id = \S::get( 'product_id' );
|
||
$field = \S::get( 'field' );
|
||
$provider = \S::get( 'provider' ) ?: 'openai';
|
||
|
||
if ( $provider === 'claude' )
|
||
{
|
||
if ( \services\GoogleAdsApi::get_setting( 'claude_enabled' ) === '0' )
|
||
{
|
||
echo json_encode( [ 'status' => 'error', 'message' => 'Claude jest wyłączony. Włącz go w Ustawieniach.' ] );
|
||
exit;
|
||
}
|
||
if ( !\services\ClaudeApi::is_configured() )
|
||
{
|
||
echo json_encode( [ 'status' => 'error', 'message' => 'Klucz API Claude nie jest skonfigurowany. Przejdź do Ustawień.' ] );
|
||
exit;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if ( \services\GoogleAdsApi::get_setting( 'openai_enabled' ) === '0' )
|
||
{
|
||
echo json_encode( [ 'status' => 'error', 'message' => 'OpenAI jest wyłączony. Włącz go w Ustawieniach.' ] );
|
||
exit;
|
||
}
|
||
if ( !\services\OpenAiApi::is_configured() )
|
||
{
|
||
echo json_encode( [ 'status' => 'error', 'message' => 'Klucz API OpenAI nie jest skonfigurowany. Przejdź do Ustawień.' ] );
|
||
exit;
|
||
}
|
||
}
|
||
|
||
$product = \factory\Products::get_product_full_context( $product_id );
|
||
if ( !$product )
|
||
{
|
||
echo json_encode( [ 'status' => 'error', 'message' => 'Nie znaleziono produktu.' ] );
|
||
exit;
|
||
}
|
||
|
||
// Pobierz treść strony produktu jeśli podano URL
|
||
$product_url = \S::get( 'product_url' );
|
||
$page_content = '';
|
||
if ( $product_url && filter_var( $product_url, FILTER_VALIDATE_URL ) )
|
||
{
|
||
$page_content = \services\OpenAiApi::fetch_page_content( $product_url );
|
||
}
|
||
|
||
$context = [
|
||
'original_name' => $product['name'],
|
||
'current_title' => \factory\Products::get_product_data( $product_id, 'title' ),
|
||
'current_description' => \factory\Products::get_product_data( $product_id, 'description' ),
|
||
'current_category' => \factory\Products::get_product_data( $product_id, 'google_product_category' ),
|
||
'offer_id' => $product['offer_id'],
|
||
'impressions_30' => $product['impressions_30'] ?? 0,
|
||
'clicks_30' => $product['clicks_30'] ?? 0,
|
||
'ctr' => $product['ctr'] ?? 0,
|
||
'cost' => $product['cost'] ?? 0,
|
||
'conversions' => $product['conversions'] ?? 0,
|
||
'conversions_value' => $product['conversions_value'] ?? 0,
|
||
'roas' => $product['roas'] ?? 0,
|
||
'custom_label_4' => \factory\Products::get_product_data( $product_id, 'custom_label_4' ),
|
||
'page_content' => $page_content,
|
||
];
|
||
|
||
$api = $provider === 'claude' ? \services\ClaudeApi::class : \services\OpenAiApi::class;
|
||
|
||
switch ( $field )
|
||
{
|
||
case 'title':
|
||
$result = $api::suggest_title( $context );
|
||
break;
|
||
case 'description':
|
||
$result = $api::suggest_description( $context );
|
||
break;
|
||
case 'category':
|
||
$result = $api::suggest_category( $context );
|
||
break;
|
||
default:
|
||
$result = [ 'status' => 'error', 'message' => 'Nieznane pole: ' . $field ];
|
||
}
|
||
|
||
$result['provider'] = $provider;
|
||
|
||
if ( $product_url && !$page_content )
|
||
$result['warning'] = 'Nie udało się pobrać treści ze strony produktu (strona może blokować dostęp). Sugestia oparta tylko na nazwie produktu.';
|
||
elseif ( $page_content )
|
||
$result['page_fetched'] = true;
|
||
|
||
echo json_encode( $result );
|
||
exit;
|
||
}
|
||
|
||
|
||
static public function get_products()
|
||
{
|
||
$client_id = \S::get( 'client_id' );
|
||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||
$limit = \S::get( 'length' ) ? \S::get( 'length' ) : 10;
|
||
$start = \S::get( 'start' ) ? \S::get( 'start' ) : 0;
|
||
|
||
$order_dir = $_POST['order'][0]['dir'] ? strtoupper( $_POST['order'][0]['dir'] ) : 'DESC';
|
||
$order_name = $_POST['order'][0]['name'] ? $_POST['order'][0]['name'] : 'clicks';
|
||
$search = $_POST['search']['value'];
|
||
|
||
// ➊ MIN/MAX ROAS dla kontekstu klienta (opcjonalnie z filtrem search)
|
||
$bounds = \factory\Products::get_roas_bounds( (int) $client_id, $search, $campaign_id, $ad_group_id );
|
||
$roas_min = (float)$bounds['min'];
|
||
$roas_max = (float)$bounds['max'];
|
||
// zabezpieczenie przed dzieleniem przez 0
|
||
if ($roas_min === $roas_max) {
|
||
$roas_max = $roas_min + 0.000001;
|
||
}
|
||
|
||
// ➋ Helper do paska performance (lokalna funkcja)
|
||
$renderPerfBar = function (float $value, float $min, float $max): string
|
||
{
|
||
// normalizacja 0..1
|
||
$t = ($value - $min) / ($max - $min);
|
||
if ($t < 0) $t = 0;
|
||
if ($t > 1) $t = 1;
|
||
|
||
// szerokości
|
||
$minPx = 20; // minimalna długość paska
|
||
$maxPx = 120; // szerokość „toru”
|
||
$fill = (int)round($minPx + $t * ($maxPx - $minPx));
|
||
|
||
// kolor od #E74C3C (czerwony) do #2ECC71 (zielony)
|
||
$from = [231, 76, 60];
|
||
$to = [ 46,204,113];
|
||
$r = (int)round($from[0] + ($to[0] - $from[0]) * $t);
|
||
$g = (int)round($from[1] + ($to[1] - $from[1]) * $t);
|
||
$b = (int)round($from[2] + ($to[2] - $from[2]) * $t);
|
||
$hex = sprintf('#%02X%02X%02X', $r, $g, $b);
|
||
|
||
// prosty pasek (tor + wypełnienie)
|
||
return '<div class="roas-perf-wrap" title="Min: '.number_format($min,2).' | Max: '.number_format($max,2).'" style="margin-top:4px;width:'.$maxPx.'px;height:8px;background:#eee;border-radius:4px;overflow:hidden;">
|
||
<div class="roas-perf-fill" style="height:100%;width:'.$fill.'px;background:'.$hex.';"></div>
|
||
</div>';
|
||
};
|
||
|
||
$db_results = \factory\Products::get_products( $client_id, $search, $limit, $start, $order_name, $order_dir, $campaign_id, $ad_group_id );
|
||
$recordsTotal = \factory\Products::get_records_total_products( $client_id, $search, $campaign_id, $ad_group_id );
|
||
|
||
$data['draw'] = \S::get( 'draw' );
|
||
$data['recordsTotal'] = $recordsTotal;
|
||
$data['recordsFiltered'] = $recordsTotal;
|
||
$data['data'] = [];
|
||
|
||
foreach ( $db_results as $row )
|
||
{
|
||
$custom_class = '';
|
||
$custom_label_4 = \factory\Products::get_product_data( $row['product_id'], 'custom_label_4' );
|
||
$custom_name = \factory\Products::get_product_data( $row['product_id'], 'title' );
|
||
|
||
if ( $custom_name )
|
||
{
|
||
$row['name'] = $custom_name;
|
||
$custom_class = 'custom_name';
|
||
}
|
||
|
||
if ( $custom_label_4 == 'deleted' )
|
||
$custom_class = 'text-danger';
|
||
|
||
$custom_label_4_color = '';
|
||
if ( $custom_label_4 == 'bestseller' )
|
||
$custom_label_4_color = 'background-color:rgb(96, 119, 102); color: #FFF;';
|
||
else if ( $custom_label_4 == 'deleted' )
|
||
$custom_label_4_color = 'background-color:rgb(255, 0, 0); color: #FFF;';
|
||
else if ( $custom_label_4 == 'zombie' )
|
||
$custom_label_4_color = 'background-color:rgb(58, 58, 58); color: #FFF;';
|
||
else if ( $custom_label_4 == 'pla_single' )
|
||
$custom_label_4_color = 'background-color:rgb(49, 184, 9); color: #FFF;';
|
||
else if ( $custom_label_4 == 'pla' )
|
||
$custom_label_4_color = 'background-color:rgb(74, 63, 136); color: #FFF;';
|
||
else if ( $custom_label_4 == 'paused' )
|
||
$custom_label_4_color = 'background-color:rgb(143, 143, 143); color: #FFF;';
|
||
|
||
// ➌ ROAS – liczba + pasek performance
|
||
$roasValue = (float)$row['roas'];
|
||
$roasNumeric = ($roasValue <= (float)$row['min_roas'])
|
||
? '<span class="text-danger text-bold">'.($roasValue).'</span>'
|
||
: $roasValue;
|
||
|
||
$roasPerfBar = $renderPerfBar($roasValue, $roas_min, $roas_max);
|
||
$roasCellHtml = '<div class="roas-cell">'.$roasNumeric.$roasPerfBar.'</div>';
|
||
|
||
$data['data'][] = [
|
||
'', // checkbox column
|
||
$row['product_id'],
|
||
$row['offer_id'],
|
||
htmlspecialchars( (string) ( $row['campaign_name'] ?? '' ) ),
|
||
htmlspecialchars( (string) ( $row['ad_group_name'] ?? '' ) ),
|
||
'<div class="table-product-title" product_id="' . $row['product_id'] . '">
|
||
<a href="/products/product_history/client_id=' . $client_id . '&product_id=' . $row['product_id'] . '&campaign_id=' . (int) ( $row['campaign_id'] ?? 0 ) . '&ad_group_id=' . (int) ( $row['ad_group_id'] ?? 0 ) . '" target="_blank" class="' . $custom_class . '">
|
||
' . $row['name'] . '
|
||
</a>
|
||
<span class="edit-product-title" product_id="' . $row['product_id'] . '">
|
||
<i class="fa fa-pencil"></i>
|
||
</span>
|
||
</div>',
|
||
$row['impressions'],
|
||
$row['impressions_30'],
|
||
'<span style="color: ' . ( $row['clicks'] > 200 ? ( $row['clicks'] > 400 ? '#0047ccff' : '#57b951' ) : '' ) . '">' . $row['clicks'] . '</span>',
|
||
$row['clicks_30'],
|
||
round( $row['ctr'], 2 ) . '%',
|
||
\S::number_display( $row['cost'] ),
|
||
\S::number_display( $row['cpc'] ),
|
||
round( $row['conversions'], 2 ),
|
||
\S::number_display( $row['conversions_value'] ),
|
||
$roasCellHtml,
|
||
'<input type="text" class="form-control min_roas" product_id="' . $row['product_id'] . '" value="' . $row['min_roas'] . '" style="width: 100px;">',
|
||
'',
|
||
'<input type="text" class="form-control custom_label_4" product_id="' . $row['product_id'] . '" value="' . $custom_label_4 . '" style="' . $custom_label_4_color . '">',
|
||
'<button type="button" class="btn btn-danger btn-sm delete-product" product_id="' . $row['product_id'] . '"><i class="fa-solid fa-trash"></i></button>'
|
||
];
|
||
}
|
||
|
||
echo json_encode( $data );
|
||
exit;
|
||
}
|
||
|
||
static public function delete_product() {
|
||
$product_id = \S::get( 'product_id' );
|
||
|
||
if ( \factory\Products::delete_product( $product_id ) )
|
||
echo json_encode( [ 'status' => 'ok' ] );
|
||
else
|
||
echo json_encode( [ 'status' => 'error' ] );
|
||
exit;
|
||
}
|
||
|
||
static public function delete_products() {
|
||
$product_ids = \S::get( 'product_ids' );
|
||
|
||
if ( !is_array( $product_ids ) || empty( $product_ids ) ) {
|
||
echo json_encode( [ 'status' => 'error', 'message' => 'Brak produktów do usunięcia' ] );
|
||
exit;
|
||
}
|
||
|
||
if ( \factory\Products::delete_products( $product_ids ) )
|
||
echo json_encode( [ 'status' => 'ok' ] );
|
||
else
|
||
echo json_encode( [ 'status' => 'error', 'message' => 'Błąd podczas usuwania produktów' ] );
|
||
exit;
|
||
}
|
||
|
||
static public function save_min_roas()
|
||
{
|
||
$product_id = \S::get( 'product_id' );
|
||
$min_roas = \S::get( 'min_roas' );
|
||
|
||
if ( \factory\Products::save_min_roas( $product_id, $min_roas ) )
|
||
{
|
||
echo json_encode( [ 'status' => 'ok' ] );
|
||
}
|
||
else
|
||
echo json_encode( [ 'status' => 'error' ] );
|
||
exit;
|
||
}
|
||
|
||
static public function save_custom_label_4()
|
||
{
|
||
$product_id = \S::get( 'product_id' );
|
||
$custom_label_4 = \S::get( 'custom_label_4' );
|
||
|
||
if ( \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 );
|
||
echo json_encode( [ 'status' => 'ok' ] );
|
||
}
|
||
else
|
||
echo json_encode( [ 'status' => 'error' ] );
|
||
exit;
|
||
}
|
||
|
||
static public function product_history()
|
||
{
|
||
$client_id = \S::get( 'client_id' );
|
||
$product_id = \S::get( 'product_id' );
|
||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||
|
||
return \Tpl::view( 'products/product_history', [
|
||
'client_id' => $client_id,
|
||
'product_id' => $product_id,
|
||
'campaign_id' => $campaign_id,
|
||
'ad_group_id' => $ad_group_id,
|
||
'min_roas' => \factory\Products::get_min_roas( $product_id )
|
||
] );
|
||
}
|
||
|
||
static public function get_product_history_table()
|
||
{
|
||
$client_id= \S::get( 'client_id' );
|
||
$product_id = \S::get( 'product_id' );
|
||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||
$start = \S::get( 'start' ) ? \S::get( 'start' ) : 0;
|
||
$limit = \S::get( 'length' ) ? \S::get( 'length' ) : 10;
|
||
|
||
$db_results = \factory\Products::get_product_history( $client_id, $product_id, $start, $limit, $campaign_id, $ad_group_id );
|
||
$recordsTotal = \factory\Products::get_records_total_product_history( $client_id, $product_id, $campaign_id, $ad_group_id );
|
||
|
||
$data['draw'] = \S::get( 'draw' );
|
||
$data['recordsTotal'] = $recordsTotal;
|
||
$data['recordsFiltered'] = $recordsTotal;
|
||
$data['data'] = [];
|
||
|
||
foreach ( $db_results as $row )
|
||
{
|
||
$roas_value = ( $row['cost'] > 0) ? ( $row['conversions_value'] / $row['cost'] ) * 100 : 0;
|
||
$roas = number_format( $roas_value, 0, '.', '' ) . '%';
|
||
|
||
$comment_data = \factory\Products::get_product_comment_by_date( $product_id, $row['date_add'] );
|
||
$comment_html = '';
|
||
if ( $comment_data )
|
||
{
|
||
$comment_html = '<div class="comment-cell">
|
||
<span class="comment-text">' . htmlspecialchars( $comment_data['comment'] ) . '</span>
|
||
<a href="#" class="text-danger delete-comment" data-comment-id="' . $comment_data['id'] . '" style="margin-left: 10px;">Usuń</a>
|
||
</div>';
|
||
}
|
||
|
||
$data['data'][] = [
|
||
$row['id'],
|
||
$row['impressions'],
|
||
$row['clicks'],
|
||
round( $row['ctr'], 2 ) . '%',
|
||
\S::number_display( $row['cost'] ),
|
||
$row['conversions'],
|
||
\S::number_display( $row['conversions_value'] ),
|
||
$roas,
|
||
$row['date_add'],
|
||
$comment_html,
|
||
];
|
||
}
|
||
|
||
echo json_encode( $data );
|
||
exit;
|
||
}
|
||
|
||
static public function get_product_history_table_chart()
|
||
{
|
||
$client_id = \S::get( 'client_id' );
|
||
$product_id = \S::get( 'product_id' );
|
||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||
$limit = \S::get( 'length' ) ? \S::get( 'length' ) : 360;
|
||
$start = \S::get( 'start' ) ? \S::get( 'start' ) : 0;
|
||
|
||
$db_results = \factory\Products::get_product_history_30( $client_id, $product_id, $start, $limit, $campaign_id, $ad_group_id );
|
||
|
||
$impressions = [];
|
||
$clicks = [];
|
||
$ctr = [];
|
||
$cost = [];
|
||
$conversions = [];
|
||
$conversions_value = [];
|
||
$roas = [];
|
||
$dates = [];
|
||
|
||
foreach ( $db_results as $row )
|
||
{
|
||
$impressions[] = (int)$row['impressions'];
|
||
$clicks[] = (int)$row['clicks'];
|
||
$ctr[] = (float)$row['ctr'];
|
||
$cost[] = (float)$row['cost'];
|
||
$conversions[] = (int)$row['conversions'];
|
||
$conversions_value[] = (float)$row['conversions_value'];
|
||
$roas[] = (float)$row['roas_all_time'];
|
||
$dates[] = $row['date_add'];
|
||
}
|
||
|
||
$chart_data = [
|
||
[
|
||
'name' => 'Wyświetlenia',
|
||
'data' => $impressions,
|
||
'visible' => false
|
||
], [
|
||
'name' => 'Kliknięcia',
|
||
'data' => $clicks,
|
||
'visible' => false
|
||
], [
|
||
'name' => 'CTR',
|
||
'data' => $ctr,
|
||
'visible' => false
|
||
], [
|
||
'name' => 'Koszt',
|
||
'data' => $cost,
|
||
'visible' => false
|
||
], [
|
||
'name' => 'Konwersje',
|
||
'data' => $conversions,
|
||
'visible' => false
|
||
], [
|
||
'name' => 'Wartość konwersji',
|
||
'data' => $conversions_value,
|
||
'visible' => false
|
||
], [
|
||
'name' => 'ROAS',
|
||
'data' => $roas
|
||
]
|
||
];
|
||
|
||
echo json_encode([
|
||
'chart_data' => $chart_data,
|
||
'dates' => $dates,
|
||
'comments' => \factory\Products::get_product_comments( $product_id ),
|
||
]);
|
||
exit;
|
||
}
|
||
|
||
static public function save_product_data()
|
||
{
|
||
$product_id = \S::get( 'product_id' );
|
||
$custom_title = \S::get( 'custom_title' );
|
||
$custom_description = \S::get( 'custom_description' );
|
||
$google_product_category = \S::get( 'google_product_category' );
|
||
$product_url = \S::get( 'product_url' );
|
||
|
||
if ( $product_id )
|
||
{
|
||
if ( $custom_title )
|
||
\factory\Products::set_product_data( $product_id, 'title', $custom_title );
|
||
|
||
if ( $custom_description )
|
||
\factory\Products::set_product_data( $product_id, 'description', $custom_description );
|
||
|
||
if ( $google_product_category )
|
||
\factory\Products::set_product_data( $product_id, 'google_product_category', $google_product_category );
|
||
|
||
\factory\Products::set_product_data( $product_id, 'product_url', $product_url ?: '' );
|
||
}
|
||
|
||
\factory\Products::add_product_comment( $product_id, 'Zmiana tytułu i opisu produktu.' );
|
||
|
||
echo json_encode( [ 'status' => 'ok' ] );
|
||
exit;
|
||
}
|
||
}
|