Files
adsPRO/autoload/controls/class.Products.php
Jacek Pyziak 4635cefcbb feat: update font to Roboto across templates and add campaign/ad group filters in product views
- 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.
2026-02-18 01:21:22 +01:00

547 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}