Add comment form alongside chart in product history

- Introduced a new layout for the product history page, integrating a comment form next to the chart.
- Styled the comment form with SCSS for better user experience.
- Implemented form submission via AJAX to save comments without page reload.
- Set default date in the comment form to today's date.
- Enhanced error handling for AJAX requests related to comment submission.
This commit is contained in:
2025-08-20 23:59:56 +02:00
parent 9e31461073
commit e0ac1cf118
6 changed files with 239 additions and 104 deletions

View File

@@ -36,6 +36,21 @@ class Products
] );
}
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 get_products()
{
$client_id = \S::get( 'client_id' );
@@ -95,7 +110,7 @@ class Products
</div>',
$row['impressions'],
$row['impressions_30'],
'<span style="color: ' . ( $row['clicks'] > 100 ? '#57b951' : '' ) . '">' . $row['clicks'] . '</span>',
'<span style="color: ' . ( $row['clicks'] > 200 ? '#57b951' : '' ) . '">' . $row['clicks'] . '</span>',
$row['clicks_30'],
round( $row['ctr'], 2 ) . '%',
\S::number_display( $row['cost'] ),
@@ -134,7 +149,7 @@ class Products
if ( \factory\Products::set_product_data( $product_id, 'custom_label_4', $custom_label_4 ) )
{
\factory\Products::add_product_comment( $product_id, 1, 'Zmiana etykiety 4 na: ' . $custom_label_4 );
\factory\Products::add_product_comment( $product_id, 'Zmiana etykiety 4 na: ' . $custom_label_4 );
echo json_encode( [ 'status' => 'ok' ] );
}
else
@@ -254,7 +269,7 @@ class Products
echo json_encode([
'chart_data' => $chart_data,
'dates' => $dates,
'comments' => []
'comments' => \factory\Products::get_product_comments( $product_id ),
]);
exit;
}
@@ -266,7 +281,7 @@ class Products
if ( \factory\Products::set_product_data( $product_id, 'title', $custom_title ) )
{
\factory\Products::add_product_comment( $product_id, 1, 'Zmiana nazwy produktu na: ' . $custom_title );
\factory\Products::add_product_comment( $product_id, 'Zmiana nazwy produktu na: ' . $custom_title );
echo json_encode( [ 'status' => 'ok' ] );
}
else

View File

@@ -2,6 +2,12 @@
namespace factory;
class Products
{
static public function get_product_comments( $product_id )
{
global $mdb;
return $mdb -> query( 'SELECT comment, date_add FROM products_comments WHERE product_id = \'' . $product_id . '\' ORDER BY date_add DESC' ) -> fetchAll( \PDO::FETCH_ASSOC );
}
static public function get_min_roas( $product_id )
{
global $mdb;
@@ -93,13 +99,18 @@ class Products
return $mdb -> query( 'SELECT SUM(clicks) FROM products_history WHERE product_id = \'' . $product_id . '\' AND date_add >= \'' . date( 'Y-m-d', strtotime( '-30 days', time() ) ) . '\'' ) -> fetchColumn();
}
static public function add_product_comment( $product_id, $type, $comment )
static public function add_product_comment( $product_id, $comment, $date = null )
{
global $mdb;
if ( $mdb -> count( 'products_comments', [ 'AND' => [ 'product_id' => $product_id, 'type' => $type, 'comment' => $comment, 'date_add' => date( 'Y-m-d' ) ] ] ) )
$mdb -> update( 'products_comments', [ 'date_add' => date( 'Y-m-d H:i:s' ) ], [ 'AND' => [ 'product_id' => $product_id, 'type' => $type, 'comment' => $comment, 'date_add' => date( 'Y-m-d' ) ] ] );
if ( !$date )
$date = date( 'Y-m-d' );
else
$mdb -> insert( 'products_comments', [ 'product_id' => $product_id, 'type' => $type, 'comment' => $comment, 'date_add' => date( 'Y-m-d' ) ] );
$date = date( 'Y-m-d', strtotime( $date ) );
if ( $mdb -> count( 'products_comments', [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] ) )
return $mdb -> update( 'products_comments', [ 'comment' => $comment ], [ 'AND' => [ 'product_id' => $product_id, 'date_add' => $date ] ] );
else
return $mdb -> insert( 'products_comments', [ 'product_id' => $product_id, 'comment' => $comment, 'date_add' => $date ] );
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1273,4 +1273,66 @@ table {
}
}
}
}
.chart-with-form {
display: flex;
gap: 20px;
align-items: flex-start;
}
.chart-area {
flex: 1 1 auto;
min-width: 0;
}
.comment-form {
width: 360px;
/* stała, wygodna szerokość prawej kolumny */
flex: 0 0 360px;
}
.comment-form .form-group {
margin-bottom: 12px;
}
.comment-form label {
display: block;
font-weight: 600;
margin-bottom: 6px;
}
.comment-form input[type="date"],
.comment-form textarea {
width: 100%;
border: 1px solid #ccc;
border-radius: 4px;
padding: 0 8px;
font-size: 14px;
}
.comment-form textarea {
min-height: 120px;
resize: vertical;
}
.comment-form .btn {
display: inline-block;
padding: 8px 14px;
border-radius: 4px;
border: 0;
background: #337ab7;
color: #fff;
font-weight: 600;
cursor: pointer;
}
.comment-form .btn[disabled] {
opacity: .6;
cursor: not-allowed;
}
.comment-form .hint {
font-size: 12px;
color: #666;
}

View File

@@ -1,12 +1,32 @@
<div class="admin-form theme-primary">
<div class="panel heading-border panel-primary">
<div class="panel-body">
<figure class="highcharts-figure">
<div id="container"></div>
</figure>
<div class="chart-with-form">
<div class="chart-area">
<figure class="highcharts-figure">
<div id="container"></div>
</figure>
</div>
<!-- PRAWY PANEL: formularz komentarza -->
<aside class="comment-form admin-form theme-primary">
<form id="product-comment-form" autocomplete="off">
<div class="form-group">
<label for="comment_date">Data</label>
<input type="date" id="comment_date" name="date" required>
</div>
<div class="form-group">
<label for="comment_text">Komentarz</label>
<textarea id="comment_text" name="comment" placeholder="Wpisz komentarz..." required></textarea>
</div>
<button type="submit" class="btn" id="save_comment">Zapisz komentarz</button>
</form>
</aside>
</div>
</div>
</div>
</div>
<div class="admin-form theme-primary">
<div class="panel heading-border panel-primary">
<div class="panel-body">
@@ -24,24 +44,34 @@
<th scope="col">Data</th>
</tr>
</thead>
<tbody>
</tbody>
<tbody></tbody>
</table>
</div>
</div>
</div>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script type="text/javascript">
$( function() {
var client_id = <?= $this -> client_id;?>;
$(function() {
var client_id = <?= $this -> client_id;?>;
var product_id = <?= $this -> product_id;?>;
table = $( '#products' ).DataTable();
// Ustaw domyślnie dzisiejszą datę w formularzu (YYYY-MM-DD)
(function presetToday() {
var el = document.getElementById('comment_date');
if (!el) return;
var d = new Date();
var m = String(d.getMonth() + 1).padStart(2, '0');
var day = String(d.getDate()).padStart(2, '0');
el.value = d.getFullYear() + '-' + m + '-' + day;
})();
// Inicjalizacja tabeli
var table = $('#products').DataTable();
table.destroy();
new DataTable( '#products', {
new DataTable('#products', {
ajax: {
type: 'POST',
url: '/products/get_product_history_table/client_id=' + client_id + '&product_id=' + product_id,
@@ -49,79 +79,40 @@
processing: true,
serverSide: true,
searching: false,
columns: [{
width: '100px',
orderable: false
},
{
width: '100px',
name: 'impressions'
},
{
width: '100px',
name: 'clicks'
},
{
width: '100px',
name: 'ctr',
className: "dt-type-numeric"
},
{
width: '100px',
name: 'cost',
className: "dt-type-numeric"
},
{
width: '100px',
name: 'conversions'
},
{
width: '175px',
name: 'conversions_value',
className: "dt-type-numeric"
},
{
width: '100px',
name: 'roas',
className: "dt-type-numeric",
orderable: false
},
{
width: '100px',
name: 'date_add'
},
columns: [
{ width: '100px', orderable: false },
{ width: '100px', name: 'impressions' },
{ width: '100px', name: 'clicks' },
{ width: '100px', name: 'ctr', className: "dt-type-numeric" },
{ width: '100px', name: 'cost', className: "dt-type-numeric" },
{ width: '100px', name: 'conversions' },
{ width: '175px', name: 'conversions_value', className: "dt-type-numeric" },
{ width: '100px', name: 'roas', className: "dt-type-numeric", orderable: false },
{ width: '100px', name: 'date_add' },
],
order: [
[ 0, false],
[ 8, 'desc']
]
order: [[0, false],[8, 'desc']]
});
// WYKRES
$.ajax({
url: '/products/get_product_history_table_chart/',
method: 'POST',
data: {
client_id: client_id,
product_id: product_id,
},
data: { client_id: client_id, product_id: product_id },
success: function(response) {
const parsedData = JSON.parse(response);
let plotLines = [];
parsedData.comments.forEach(function(comment) {
(parsedData.comments || []).forEach(function(comment) {
plotLines.push({
color: '#333333',
width: 1,
value: parsedData.dates.indexOf(comment.date_add.split(' ')[0]),
value: parsedData.dates.indexOf((comment.date_add || '').split(' ')[0]),
dashStyle: 'Solid',
label: {
text: comment.comment,
text: comment.comment, // trzyma przy lewej krawędzi linii
rotation: 0,
align: 'left',
style: {
color: '#333333',
fontSize: '14px'
}
style: { color: '#333333', fontSize: '14px' }
},
zIndex: 5
});
@@ -130,8 +121,6 @@
const chart = Highcharts.chart('container', {
title: { text: `` },
subtitle: { text: `` },
// (możesz zostawić lub usunąć warunkowy blok yAxis z PHP — ten kod zadziała niezależnie)
xAxis: {
categories: parsedData.dates,
labels: {
@@ -156,25 +145,56 @@
verticalAlign: 'middle',
itemStyle: { fontSize: '14px' }
},
plotOptions: {
series: { label: { connectorAllowed: false }, pointStart: 0 }
},
plotOptions: { series: { label: { connectorAllowed: false }, pointStart: 0 } },
series: parsedData.chart_data,
tooltip: { style: { fontSize: '14px' } }
});
// >>> DODAJ TO PO UTWORZENIU WYKRESU <<<
<?php if ($this->min_roas): ?>
chart.xAxis[0].update({
plotLines: (chart.xAxis[0].options.plotLines || []).map(function(pl) {
return Highcharts.merge(pl, { label: { text: '' } });
})
}, false);
// 2) Dodaj „niewidzialne” markery do hovera
var points = (parsedData.comments || []).map(function(c) {
var idx = parsedData.dates.indexOf((c.date_add || '').split(' ')[0]);
return { x: idx, y: 0, comment: c.comment };
});
chart.addSeries({
type: 'scatter',
name: 'Komentarz:',
data: points,
yAxis: 0,
tooltip: {
pointFormatter: function () {
return '<span style="font-size: 12px;">' + (this.comment || '') + '</span>';
}
},
marker: {
enabled: true,
radius: 4,
lineWidth: 0,
fillOpacity: 0 // marker „niewidoczny”
},
states: {
hover: { enabled: true, halo: { size: 5 } }
},
enableMouseTracking: true
}, false);
chart.redraw();
// MIN ROAS linia + dopasowanie osi
<?php if ($this->min_roas): ?>
(function() {
var limitVal = Number(<?= json_encode($this->min_roas) ?>);
var roasSeries = chart.series.find(function(s) {
return (s.name || '').toLowerCase() === 'roas';
});
var targetAxis = roasSeries ? roasSeries.yAxis : chart.yAxis[0];
// dodanie linii
targetAxis.addPlotLine({
id: 'min-roas-line',
color: 'red',
@@ -195,20 +215,15 @@
var min = Math.min(e.dataMin, val);
var max = Math.max(e.dataMax, val);
var span = (max - min) || 1;
var pad = span * 0.05;
var newMin = min - pad;
var newMax = max + pad;
// uniknij niepotrzebnych setExtremes
if (newMin !== e.min || newMax !== e.max) {
axis.setExtremes(newMin, newMax); // domyślnie robi redraw
axis.setExtremes(newMin, newMax);
}
}
// 1) po załadowaniu wykresu (jeśli już załadowany od razu)
if (chart.hasLoaded) {
adjustAxisToIncludeValue(targetAxis, limitVal);
} else {
@@ -217,22 +232,54 @@
});
}
// 2) przy show/hide serii (klik w legendzie)
chart.series.forEach(function (s) {
Highcharts.addEvent(s, 'show', function () {
adjustAxisToIncludeValue(targetAxis, limitVal);
});
Highcharts.addEvent(s, 'hide', function () {
adjustAxisToIncludeValue(targetAxis, limitVal);
});
Highcharts.addEvent(s, 'show', function () { adjustAxisToIncludeValue(targetAxis, limitVal); });
Highcharts.addEvent(s, 'hide', function () { adjustAxisToIncludeValue(targetAxis, limitVal); });
});
})();
<?php endif; ?>
},
error: function (jqXHR, textStatus, errorThrown) {
console.error('Error AJAX:', textStatus, errorThrown);
}
})
})
</script>
});
// OBSŁUGA FORMULARZA zapis komentarza i odświeżenie strony
$('#product-comment-form').on('submit', function(e) {
e.preventDefault();
var $btn = $('#save_comment');
var dateStr = $.trim($('#comment_date').val());
var comment = $.trim($('#comment_text').val());
if (!dateStr) { alert('Wybierz datę.'); return; }
if (!comment) { alert('Wpisz komentarz.'); return; }
$btn.prop('disabled', true).text('Zapisywanie...');
$.ajax({
url: '/products/comment_add/',
method: 'POST',
data: {
client_id: client_id,
product_id: product_id,
date: dateStr, // oczekiwany format: YYYY-MM-DD
comment: comment
},
success: function(res) {
res = JSON.parse(res);
if (res.status === 'ok') {
location.reload();
} else {
alert('Nie udało się zapisać komentarza. Spróbuj ponownie.');
}
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Błąd zapisu komentarza:', textStatus, errorThrown);
alert('Nie udało się zapisać komentarza. Spróbuj ponownie.');
$btn.prop('disabled', false).text('Zapisz komentarz');
}
});
});
});
</script>