Files
site_aegisone/py_service/app/templates/pages/charts.html
T

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 %}