Files
sodino/admin/js/dashboard.js

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);
});