223 lines
10 KiB
HTML
223 lines
10 KiB
HTML
{% extends "page.html" %}
|
|
{% block page_content %}
|
|
<style>
|
|
.modal-overlay { display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); z-index:1000; align-items:center; justify-content:center; }
|
|
.modal-overlay.open { display:flex; }
|
|
.modal { background:var(--bg-card); border:1px solid var(--border); border-radius:12px; padding:24px; width:420px; max-width:90vw; }
|
|
.modal h2 { color:var(--text-primary); }
|
|
.modal p { color:var(--text-secondary); }
|
|
.form-actions { display:flex; gap:10px; }
|
|
.btn-danger { background:var(--danger, #dc3545); color:#fff; border:none; padding:8px 16px; border-radius:6px; cursor:pointer; font-weight:600; }
|
|
.btn-primary { background:var(--accent, #00ADEF); color:#fff; border:none; padding:8px 16px; border-radius:6px; cursor:pointer; font-weight:600; }
|
|
.btn { background:var(--bg-input); color:var(--text-primary); border:1px solid var(--border); padding:8px 16px; border-radius:6px; cursor:pointer; }
|
|
.form-group { margin-bottom:0; }
|
|
</style>
|
|
<div class="card">
|
|
<div class="card-header"><h2>Графики и диаграммы</h2></div>
|
|
<div class="filter-bar" style="flex-wrap:wrap;gap:8px;">
|
|
<select id="metric-select">
|
|
<option value="tasks">Задачи по месяцам</option>
|
|
<option value="incidents">Инциденты по severity</option>
|
|
<option value="shs">SHS история</option>
|
|
<option value="kpi">KPI инженеров</option>
|
|
<option value="mrr">MRR по статусам</option>
|
|
<option value="objects">Объекты по статусам</option>
|
|
</select>
|
|
<select id="chart-type">
|
|
<option value="bar">Столбчатая</option>
|
|
<option value="line">Линейная</option>
|
|
<option value="pie">Круговая</option>
|
|
<option value="doughnut">Кольцевая</option>
|
|
</select>
|
|
<select id="chart-group">
|
|
<option value="month">По месяцам</option>
|
|
<option value="week">По неделям</option>
|
|
<option value="day">По дням</option>
|
|
</select>
|
|
<label style="font-size:12px;color:var(--text-muted);display:flex;align-items:center;gap:4px;">
|
|
<input type="text" id="chart-title" placeholder="Название графика" style="width:160px;padding:4px 8px;border:1px solid var(--border);border-radius:6px;background:var(--bg-card);color:var(--text);">
|
|
</label>
|
|
<button class="btn btn-primary btn-sm" onclick="loadChart()">Обновить</button>
|
|
<button class="btn btn-secondary btn-sm" onclick="exportChart()">Экспорт PNG</button>
|
|
</div>
|
|
<div class="filter-bar" style="margin-top:8px;flex-wrap:wrap;gap:6px;">
|
|
<span style="font-size:12px;color:var(--text-muted);">Пользовательские графики:</span>
|
|
<button class="btn btn-secondary btn-sm" onclick="saveCustomChart()">Сохранить текущий</button>
|
|
<select id="custom-chart-select" onchange="loadCustomChart(this.value)">
|
|
<option value="">— Загрузить —</option>
|
|
</select>
|
|
<button class="btn btn-danger btn-sm" onclick="deleteCustomChart()">Удалить</button>
|
|
</div>
|
|
<div style="position:relative;height:400px;">
|
|
<canvas id="chart-canvas"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-overlay" id="chart-name-modal">
|
|
<div class="modal" style="max-width:420px;">
|
|
<h2 style="font-size:16px;margin-bottom:8px;">Название графика</h2>
|
|
<div class="form-group">
|
|
<input type="text" id="chart-name-input" placeholder="Введите название..." style="width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:6px;font-size:14px;background:var(--bg-card);color:var(--text);box-sizing:border-box;">
|
|
</div>
|
|
<div class="form-actions" style="justify-content:flex-end;margin-top:16px;">
|
|
<button type="button" class="btn btn-primary" onclick="confirmSaveChart()">Сохранить</button>
|
|
<button type="button" class="btn" onclick="closeChartNameModal()">Отмена</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-overlay" id="confirm-delete-chart">
|
|
<div class="modal" style="max-width:420px;">
|
|
<h2 style="font-size:16px;margin-bottom:8px;">Удалить график?</h2>
|
|
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:20px;">
|
|
<strong id="delete-chart-name"></strong> будет удалён навсегда.
|
|
Это действие нельзя отменить.
|
|
</p>
|
|
<div class="form-actions" style="justify-content:flex-end;">
|
|
<button type="button" class="btn btn-danger" onclick="confirmDeleteChart()">Удалить</button>
|
|
<button type="button" class="btn" onclick="closeConfirmDeleteChart()">Отмена</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
|
<script>
|
|
var chartInstance = null;
|
|
var currentChartConfig = null;
|
|
var pendingDeleteChartName = null;
|
|
|
|
function loadChart(){
|
|
var metric = document.getElementById('metric-select').value;
|
|
var type = document.getElementById('chart-type').value;
|
|
var group = document.getElementById('chart-group').value;
|
|
var canvas = document.getElementById('chart-canvas');
|
|
var title = document.getElementById('chart-title').value || '';
|
|
|
|
fetch('/service/api/charts/data?metric=' + metric + '&group=' + group).then(function(r){ return r.json() }).then(function(data){
|
|
if(chartInstance) { chartInstance.destroy(); chartInstance = null; }
|
|
|
|
if(!data.labels || data.labels.length === 0){
|
|
return;
|
|
}
|
|
|
|
currentChartConfig = { metric: metric, type: type, group: group, title: title, labels: data.labels };
|
|
|
|
var datasets = data.datasets.map(function(ds){
|
|
var color = 'hsl(' + (Math.random() * 360) + ', 70%, 60%)';
|
|
return {
|
|
label: ds.label,
|
|
data: ds.data,
|
|
backgroundColor: type === 'line' ? color : (type === 'bar' ? color : [
|
|
'rgba(0,173,239,0.7)', 'rgba(34,197,94,0.7)', 'rgba(245,158,11,0.7)',
|
|
'rgba(239,68,68,0.7)', 'rgba(99,102,241,0.7)', 'rgba(236,72,153,0.7)'
|
|
]),
|
|
borderColor: color,
|
|
borderWidth: 2,
|
|
tension: 0.3,
|
|
};
|
|
});
|
|
|
|
chartInstance = new Chart(canvas, {
|
|
type: type === 'doughnut' ? 'doughnut' : (['pie', 'doughnut'].includes(type) ? type : type),
|
|
data: { labels: data.labels, datasets: datasets },
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { labels: { color: '#8899bb' } },
|
|
title: title ? { display: true, text: title, color: '#e8edf5', font: { size: 14 } } : undefined
|
|
},
|
|
scales: (['pie', 'doughnut'].includes(type) ? undefined : {
|
|
x: { ticks: { color: '#8899bb' }, grid: { color: 'rgba(255,255,255,0.05)' } },
|
|
y: { ticks: { color: '#8899bb' }, grid: { color: 'rgba(255,255,255,0.05)' } }
|
|
})
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function exportChart(){
|
|
var canvas = document.getElementById('chart-canvas');
|
|
if(!canvas) return;
|
|
var link = document.createElement('a');
|
|
link.download = 'chart_' + new Date().toISOString().slice(0,10) + '.png';
|
|
link.href = canvas.toDataURL('image/png');
|
|
link.click();
|
|
}
|
|
|
|
function saveCustomChart(){
|
|
if(!currentChartConfig) return;
|
|
document.getElementById('chart-name-input').value = '';
|
|
document.getElementById('chart-name-modal').classList.add('open');
|
|
document.getElementById('chart-name-input').focus();
|
|
}
|
|
|
|
function closeChartNameModal(){
|
|
document.getElementById('chart-name-modal').classList.remove('open');
|
|
}
|
|
|
|
function confirmSaveChart(){
|
|
var name = document.getElementById('chart-name-input').value.trim();
|
|
if(!name){ return; }
|
|
closeChartNameModal();
|
|
var saved = JSON.parse(localStorage.getItem('custom_charts') || '[]');
|
|
saved.push({ name: name, config: currentChartConfig });
|
|
localStorage.setItem('custom_charts', JSON.stringify(saved));
|
|
updateCustomChartList();
|
|
}
|
|
|
|
function loadCustomChart(name){
|
|
if(!name) return;
|
|
var saved = JSON.parse(localStorage.getItem('custom_charts') || '[]');
|
|
var found = saved.find(function(c){ return c.name === name; });
|
|
if(!found) return;
|
|
document.getElementById('metric-select').value = found.config.metric;
|
|
document.getElementById('chart-type').value = found.config.type;
|
|
document.getElementById('chart-group').value = found.config.group;
|
|
document.getElementById('chart-title').value = found.config.title || '';
|
|
loadChart();
|
|
}
|
|
|
|
function deleteCustomChart(){
|
|
var sel = document.getElementById('custom-chart-select');
|
|
var name = sel.value;
|
|
if(!name) return;
|
|
pendingDeleteChartName = name;
|
|
document.getElementById('delete-chart-name').textContent = name;
|
|
document.getElementById('confirm-delete-chart').classList.add('open');
|
|
}
|
|
|
|
function closeConfirmDeleteChart(){
|
|
document.getElementById('confirm-delete-chart').classList.remove('open');
|
|
pendingDeleteChartName = null;
|
|
}
|
|
|
|
function confirmDeleteChart(){
|
|
var name = pendingDeleteChartName;
|
|
closeConfirmDeleteChart();
|
|
var sel = document.getElementById('custom-chart-select');
|
|
var saved = JSON.parse(localStorage.getItem('custom_charts') || '[]');
|
|
saved = saved.filter(function(c){ return c.name !== name; });
|
|
localStorage.setItem('custom_charts', JSON.stringify(saved));
|
|
updateCustomChartList();
|
|
sel.value = '';
|
|
}
|
|
|
|
function updateCustomChartList(){
|
|
var sel = document.getElementById('custom-chart-select');
|
|
var saved = JSON.parse(localStorage.getItem('custom_charts') || '[]');
|
|
sel.innerHTML = '<option value="">— Загрузить —</option>';
|
|
saved.forEach(function(c){
|
|
var opt = document.createElement('option');
|
|
opt.value = c.name; opt.textContent = c.name;
|
|
sel.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function(){
|
|
loadChart();
|
|
updateCustomChartList();
|
|
});
|
|
</script>
|
|
{% endblock %}
|