refactor(Core): optimize admin panel and refactor

This commit is contained in:
2026-05-07 00:15:32 +03:30
parent dec4e67b9e
commit 7cc14b7439
25 changed files with 1162 additions and 282 deletions

View File

@@ -11,165 +11,198 @@ document.addEventListener('DOMContentLoaded', function () {
return;
}
if (typeof Chart === 'undefined') {
return;
}
const salesChartContext = document.getElementById('sodinoSalesChart');
const discountChartContext = document.getElementById('sodinoDiscountChart');
const ruleChartContext = document.getElementById('sodinoRuleChart');
if (salesChartContext && dashboardData.salesChart) {
new Chart(salesChartContext.getContext('2d'), {
type: 'line',
data: {
labels: dashboardData.salesChart.labels,
datasets: [
{
label: dashboardData.translations.afterApplying,
data: dashboardData.salesChart.after,
borderColor: '#0ea5e9',
backgroundColor: 'rgba(14, 165, 233, 0.15)',
fill: true,
tension: 0.35,
pointRadius: 3,
},
{
label: dashboardData.translations.beforeApplying,
data: dashboardData.salesChart.before,
borderColor: '#ef4444',
backgroundColor: 'rgba(239, 68, 68, 0.15)',
fill: true,
tension: 0.35,
pointRadius: 3,
},
],
},
options: {
responsive: true,
plugins: {
legend: {
labels: {
color: '#334155',
},
},
},
scales: {
x: {
ticks: {
color: '#475569',
},
grid: {
color: 'rgba(148, 163, 184, 0.15)',
},
},
y: {
ticks: {
color: '#475569',
},
grid: {
color: 'rgba(148, 163, 184, 0.15)',
},
},
},
},
function toNumbers(values) {
return (values || []).map(function (value) {
const number = Number(value);
return Number.isFinite(number) ? number : 0;
});
}
if (discountChartContext && dashboardData.summary) {
new Chart(discountChartContext.getContext('2d'), {
type: 'bar',
data: {
labels: [dashboardData.translations.totalDiscount, dashboardData.translations.totalRevenue],
datasets: [
{
label: dashboardData.translations.discountEffect,
data: [dashboardData.summary.total_discount, dashboardData.summary.total_revenue],
backgroundColor: ['#fbbf24', '#0ea5e9'],
borderRadius: 999,
borderSkipped: false,
},
],
},
options: {
responsive: true,
plugins: {
legend: {
display: false,
},
},
scales: {
x: {
ticks: {
color: '#475569',
},
grid: {
display: false,
},
},
y: {
ticks: {
color: '#475569',
},
grid: {
color: 'rgba(148, 163, 184, 0.15)',
},
},
},
},
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();
});
}
if (ruleChartContext && dashboardData.rulePerformance) {
new Chart(ruleChartContext.getContext('2d'), {
type: 'bar',
data: {
labels: dashboardData.rulePerformance.names,
datasets: [
{
label: dashboardData.translations.ruleRevenue,
data: dashboardData.rulePerformance.revenue,
backgroundColor: '#0ea5e9',
borderRadius: 999,
},
{
label: dashboardData.translations.ruleDiscount,
data: dashboardData.rulePerformance.discount,
backgroundColor: '#f59e0b',
borderRadius: 999,
},
],
},
options: {
indexAxis: 'y',
responsive: true,
plugins: {
legend: {
position: 'bottom',
labels: {
color: '#475569',
},
},
},
scales: {
x: {
ticks: {
color: '#475569',
},
grid: {
color: 'rgba(148, 163, 184, 0.15)',
},
},
y: {
ticks: {
color: '#475569',
},
grid: {
display: false,
},
},
},
},
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);
});