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

@@ -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>