- Create migration for global settings table and add google_ads_customer_id and google_ads_start_date columns to clients table. - Add migration to include product_url column in products_data table. - Insert demo data for campaigns, products, and their history for client 'pomysloweprezenty.pl'. - Implement client management interface with modals for adding and editing clients, including Google Ads Customer ID and data retrieval start date.
484 lines
17 KiB
PHP
484 lines
17 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 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' );
|
||
|
||
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,
|
||
];
|
||
|
||
switch ( $field )
|
||
{
|
||
case 'title':
|
||
$result = \services\OpenAiApi::suggest_title( $context );
|
||
break;
|
||
case 'description':
|
||
$result = \services\OpenAiApi::suggest_description( $context );
|
||
break;
|
||
case 'category':
|
||
$result = \services\OpenAiApi::suggest_category( $context );
|
||
break;
|
||
default:
|
||
$result = [ 'status' => 'error', 'message' => 'Nieznane pole: ' . $field ];
|
||
}
|
||
|
||
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' );
|
||
$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( $client_id, $search );
|
||
$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 );
|
||
$recordsTotal = \factory\Products::get_records_total_products( $client_id, $search );
|
||
|
||
$data['draw'] = \S::get( 'draw' );
|
||
$data['recordsTotal'] = $recordsTotal;
|
||
$data['recordsFiltered'] = $recordsTotal;
|
||
|
||
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'],
|
||
'<div class="table-product-title" product_id="' . $row['product_id'] . '">
|
||
<a href="/products/product_history/client_id=' . $client_id . '&product_id=' . $row['product_id'] . '" 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' );
|
||
|
||
return \Tpl::view( 'products/product_history', [
|
||
'client_id' => $client_id,
|
||
'product_id' => $product_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' );
|
||
$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 );
|
||
$recordsTotal = \factory\Products::get_records_total_product_history( $client_id, $product_id );
|
||
|
||
$data['draw'] = \S::get( 'draw' );
|
||
$data['recordsTotal'] = $recordsTotal;
|
||
$data['recordsFiltered'] = $recordsTotal;
|
||
|
||
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' );
|
||
$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 );
|
||
|
||
$impressions = [];
|
||
$clicks = [];
|
||
$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;
|
||
}
|
||
} |