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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user