209 lines
7.2 KiB
JavaScript
209 lines
7.2 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function () {
|
|
const dataEl = document.getElementById('sodino-dashboard-data');
|
|
if (!dataEl) {
|
|
return;
|
|
}
|
|
|
|
let dashboardData;
|
|
try {
|
|
dashboardData = JSON.parse(dataEl.textContent || '{}');
|
|
} catch (e) {
|
|
return;
|
|
}
|
|
|
|
function toNumbers(values) {
|
|
return (values || []).map(function (value) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number : 0;
|
|
});
|
|
}
|
|
|
|
function fitCanvas(canvas) {
|
|
const ratio = window.devicePixelRatio || 1;
|
|
const rect = canvas.getBoundingClientRect();
|
|
canvas.width = Math.max(1, Math.floor(rect.width * ratio));
|
|
canvas.height = Math.max(1, Math.floor(rect.height * ratio));
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
return { ctx, width: rect.width, height: rect.height };
|
|
}
|
|
|
|
function drawGrid(ctx, area, lines) {
|
|
ctx.strokeStyle = 'rgba(148, 163, 184, 0.22)';
|
|
ctx.lineWidth = 1;
|
|
|
|
for (let i = 0; i <= lines; i++) {
|
|
const y = area.top + (area.height / lines) * i;
|
|
ctx.beginPath();
|
|
ctx.moveTo(area.left, y);
|
|
ctx.lineTo(area.left + area.width, y);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function drawLineChart(canvas, series) {
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
const fitted = fitCanvas(canvas);
|
|
const ctx = fitted.ctx;
|
|
const width = fitted.width;
|
|
const height = fitted.height;
|
|
const area = { left: 18, top: 18, width: width - 36, height: height - 42 };
|
|
const allValues = series.reduce(function (values, item) {
|
|
return values.concat(item.values);
|
|
}, []);
|
|
const max = Math.max(1, ...allValues);
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
|
drawGrid(ctx, area, 4);
|
|
|
|
series.forEach(function (item) {
|
|
const values = item.values;
|
|
if (!values.length) {
|
|
return;
|
|
}
|
|
|
|
ctx.strokeStyle = item.color;
|
|
ctx.fillStyle = item.fill;
|
|
ctx.lineWidth = 2.5;
|
|
ctx.beginPath();
|
|
|
|
values.forEach(function (value, index) {
|
|
const x = area.left + (values.length === 1 ? area.width / 2 : (area.width / (values.length - 1)) * index);
|
|
const y = area.top + area.height - ((value / max) * area.height);
|
|
if (index === 0) {
|
|
ctx.moveTo(x, y);
|
|
} else {
|
|
ctx.lineTo(x, y);
|
|
}
|
|
});
|
|
|
|
ctx.stroke();
|
|
|
|
const lastX = area.left + area.width;
|
|
ctx.lineTo(lastX, area.top + area.height);
|
|
ctx.lineTo(area.left, area.top + area.height);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
});
|
|
}
|
|
|
|
function drawVerticalBars(canvas, labels, values, colors) {
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
const fitted = fitCanvas(canvas);
|
|
const ctx = fitted.ctx;
|
|
const width = fitted.width;
|
|
const height = fitted.height;
|
|
const area = { left: 24, top: 18, width: width - 48, height: height - 52 };
|
|
const max = Math.max(1, ...values);
|
|
const gap = 18;
|
|
const barWidth = Math.max(18, (area.width - gap * (values.length - 1)) / Math.max(1, values.length));
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
|
drawGrid(ctx, area, 4);
|
|
ctx.font = '12px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
|
|
ctx.textAlign = 'center';
|
|
ctx.fillStyle = '#475569';
|
|
|
|
values.forEach(function (value, index) {
|
|
const x = area.left + index * (barWidth + gap);
|
|
const barHeight = (value / max) * area.height;
|
|
const y = area.top + area.height - barHeight;
|
|
|
|
ctx.fillStyle = colors[index % colors.length];
|
|
roundRect(ctx, x, y, barWidth, barHeight, 8);
|
|
ctx.fill();
|
|
|
|
ctx.fillStyle = '#475569';
|
|
ctx.fillText(labels[index] || '', x + barWidth / 2, height - 16);
|
|
});
|
|
}
|
|
|
|
function drawHorizontalBars(canvas, labels, revenue, discount) {
|
|
if (!canvas) {
|
|
return;
|
|
}
|
|
|
|
const fitted = fitCanvas(canvas);
|
|
const ctx = fitted.ctx;
|
|
const width = fitted.width;
|
|
const height = fitted.height;
|
|
const area = { left: 18, top: 18, width: width - 36, height: height - 36 };
|
|
const rows = Math.max(1, labels.length);
|
|
const max = Math.max(1, ...revenue, ...discount);
|
|
const rowHeight = area.height / rows;
|
|
|
|
ctx.clearRect(0, 0, width, height);
|
|
ctx.font = '12px system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
|
|
ctx.textAlign = 'right';
|
|
|
|
labels.forEach(function (label, index) {
|
|
const y = area.top + rowHeight * index + 10;
|
|
const labelWidth = Math.min(110, area.width * 0.35);
|
|
const chartLeft = area.left;
|
|
const chartWidth = area.width - labelWidth - 12;
|
|
const revenueWidth = (Number(revenue[index] || 0) / max) * chartWidth;
|
|
const discountWidth = (Number(discount[index] || 0) / max) * chartWidth;
|
|
|
|
ctx.fillStyle = '#475569';
|
|
ctx.fillText(String(label || '').slice(0, 18), width - 18, y + 14);
|
|
|
|
ctx.fillStyle = '#0ea5e9';
|
|
roundRect(ctx, chartLeft, y, revenueWidth, 8, 6);
|
|
ctx.fill();
|
|
|
|
ctx.fillStyle = '#f59e0b';
|
|
roundRect(ctx, chartLeft, y + 13, discountWidth, 8, 6);
|
|
ctx.fill();
|
|
});
|
|
}
|
|
|
|
function roundRect(ctx, x, y, width, height, radius) {
|
|
const r = Math.min(radius, Math.abs(width) / 2, Math.abs(height) / 2);
|
|
ctx.beginPath();
|
|
ctx.moveTo(x + r, y);
|
|
ctx.lineTo(x + width - r, y);
|
|
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
|
ctx.lineTo(x + width, y + height - r);
|
|
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
|
ctx.lineTo(x + r, y + height);
|
|
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
|
ctx.lineTo(x, y + r);
|
|
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
ctx.closePath();
|
|
}
|
|
|
|
function render() {
|
|
const sales = dashboardData.salesChart || {};
|
|
drawLineChart(document.getElementById('sodinoSalesChart'), [
|
|
{ values: toNumbers(sales.after), color: '#0ea5e9', fill: 'rgba(14, 165, 233, 0.12)' },
|
|
{ values: toNumbers(sales.before), color: '#ef4444', fill: 'rgba(239, 68, 68, 0.10)' },
|
|
]);
|
|
|
|
const summary = dashboardData.summary || {};
|
|
drawVerticalBars(
|
|
document.getElementById('sodinoDiscountChart'),
|
|
[dashboardData.translations.totalDiscount, dashboardData.translations.totalRevenue],
|
|
toNumbers([summary.total_discount, summary.total_revenue]),
|
|
['#f59e0b', '#0ea5e9']
|
|
);
|
|
|
|
const rules = dashboardData.rulePerformance || {};
|
|
drawHorizontalBars(
|
|
document.getElementById('sodinoRuleChart'),
|
|
rules.names || [],
|
|
toNumbers(rules.revenue),
|
|
toNumbers(rules.discount)
|
|
);
|
|
}
|
|
|
|
render();
|
|
window.addEventListener('resize', render);
|
|
});
|