257 lines
12 KiB
HTML
257 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
{% block body_class %}dashboard-page{% endblock %}
|
|
{% block content %}
|
|
<button class="sidebar-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">☰</button>
|
|
<div class="layout">
|
|
<aside class="sidebar">
|
|
<div class="sidebar-header">
|
|
<div class="logo sidebar-logo-text">Aegis<span>One</span></div>
|
|
<div style="font-size:12px;color:var(--text-muted);letter-spacing:0.05em;">Service Portal</div>
|
|
</div>
|
|
<nav class="sidebar-nav">
|
|
<div class="sidebar-section">Панель</div>
|
|
<a href="/service/dashboard" class="active">Дашборд</a>
|
|
{% if user.role == 'owner' %}
|
|
<div class="sidebar-section">Управление</div>
|
|
<a href="/service/users">Сотрудники</a>
|
|
<a href="/service/customers">Клиенты</a>
|
|
<a href="/service/objects">Объекты</a>
|
|
<a href="/service/assignments">Назначение сотрудников</a>
|
|
<a href="/service/sla">SLA контракты</a>
|
|
<a href="/service/ceo">CEO дашборд</a>
|
|
<a href="/service/coefficients">Коэффициенты</a>
|
|
<a href="/service/charts">Графики</a>
|
|
<a href="/service/ideas">Идеи</a>
|
|
{% elif user.role == 'engineer' %}
|
|
<div class="sidebar-section">Управление</div>
|
|
<a href="/service/customers">Клиенты</a>
|
|
<a href="/service/objects">Объекты</a>
|
|
<a href="/service/sla">SLA контракты</a>
|
|
<a href="/service/documents/tech-access">Доступ техников к документам</a>
|
|
{% endif %}
|
|
<div class="sidebar-section">Работа</div>
|
|
{% if user.role in ['engineer', 'technician'] %}
|
|
<a href="/service/tasks">Задачи</a>
|
|
<a href="/service/reports">Отчёты</a>
|
|
<a href="/service/incidents">Инциденты</a>
|
|
{% endif %}
|
|
{% if user.role in ['owner', 'engineer'] %}
|
|
<a href="/service/questionnaire">Опросник</a>
|
|
{% endif %}
|
|
{% if user.role == 'owner' %}
|
|
<a href="/service/questionnaire-config">Настройка опросника</a>
|
|
{% endif %}
|
|
{% if user.role in ['owner', 'engineer'] %}
|
|
<a href="/service/passports">Паспорта объектов</a>
|
|
{% endif %}
|
|
{% if user.role == 'technician' %}
|
|
<a href="/service/checklist">Чек-лист</a>
|
|
{% endif %}
|
|
{% if user.role == 'owner' %}
|
|
<div class="sidebar-section">Внешние системы</div>
|
|
<a href="https://git.aegisone.ru" target="_blank" rel="noopener">Gitea</a>
|
|
<a href="{{ PORTAINER_URL }}" target="_blank" rel="noopener">Portainer</a>
|
|
{% endif %}
|
|
<div class="sidebar-section">Блог</div>
|
|
{% if user.role == 'owner' %}
|
|
<a href="/service/blog">Управление блогом</a>
|
|
{% endif %}
|
|
<a href="/blog/">Публичный блог</a>
|
|
<div class="sidebar-section">Контент</div>
|
|
{% if user.role == 'owner' %}
|
|
<a href="/service/cases">Примеры из практики</a>
|
|
{% endif %}
|
|
<div class="sidebar-section">Документация</div>
|
|
{% if user.role in ['owner', 'engineer'] %}
|
|
<a href="/service/documents/">Просмотр документации</a>
|
|
{% endif %}
|
|
{% if user.role == 'owner' %}
|
|
<a href="/service/documents/admin/permissions">Управление документами</a>
|
|
{% endif %}
|
|
</nav>
|
|
<div class="sidebar-footer">
|
|
<div class="user-name">{{ user.full_name }}</div>
|
|
<a href="/service/logout" style="display:inline-block;margin:6px 0;font-size:12px;">Выйти</a>
|
|
<div class="version-pill" onclick="openChangelog()" title="История изменений">v{{ app_version }}</div>
|
|
</div>
|
|
</aside>
|
|
<main class="main">
|
|
<div class="main-header">
|
|
<div>
|
|
<h1>Дашборд</h1>
|
|
<div class="breadcrumb"></div>
|
|
</div>
|
|
<div class="actions">
|
|
<span style="font-size:12px;color:var(--text-muted);">{{ now }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{% if shs_alert %}
|
|
<div class="shs-alert critical">⚠ {{ shs_alert }}</div>
|
|
{% endif %}
|
|
|
|
<div class="card-grid">
|
|
<a href="/service/objects" class="card card-link">
|
|
<div class="card-label">Активные объекты</div>
|
|
<div class="card-value value-accent">{{ stats.objects }}</div>
|
|
</a>
|
|
<a href="/service/tasks" class="card card-link">
|
|
<div class="card-label">Активные задачи</div>
|
|
<div class="card-value {% if stats.tasks > 0 %}value-yellow{% else %}value-green{% endif %}">{{ stats.tasks }}</div>
|
|
</a>
|
|
<a href="/service/incidents" class="card card-link">
|
|
<div class="card-label">Открытые инциденты</div>
|
|
<div class="card-value {% if stats.incidents > 0 %}value-red{% else %}value-green{% endif %}">{{ stats.incidents }}</div>
|
|
</a>
|
|
<a href="/service/sla" class="card card-link">
|
|
<div class="card-label">SLA контракты</div>
|
|
<div class="card-value value-info">{{ stats.sla_count }}</div>
|
|
</a>
|
|
<a href="/service/sla" class="card card-link">
|
|
<div class="card-label">SLA Compliance</div>
|
|
<div class="card-value {% if stats.sla_compliance >= 90 %}value-green{% elif stats.sla_compliance >= 70 %}value-yellow{% else %}value-red{% endif %}">{{ stats.sla_compliance }}%</div>
|
|
</a>
|
|
{% if stats.shs %}
|
|
<a href="/service/ceo" class="card card-link">
|
|
<div class="card-label">SHS (System Health)</div>
|
|
<div class="card-value {% if stats.shs.shs_status in ['Growth', 'Stable'] %}value-green{% else %}value-red{% endif %}">{{ stats.shs.shs_score }}</div>
|
|
<div class="card-label"><span class="badge badge-{{ stats.shs.shs_status|lower }}">{{ stats.shs.shs_status }}</span></div>
|
|
</a>
|
|
{% endif %}
|
|
{% if stats.my_tasks is defined %}
|
|
<a href="/service/tasks" class="card card-link">
|
|
<div class="card-label">Мои задачи</div>
|
|
<div class="card-value">{{ stats.my_tasks }}</div>
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
<div style="text-align:right;margin-top:12px;">
|
|
<button class="btn btn-secondary btn-sm" onclick="loadDashSettings();document.getElementById('modal-dash-settings').classList.add('open')">Настроить</button>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
<div class="modal-overlay" id="modal-dash-settings"><div class="modal">
|
|
<button type="button" class="modal-close" onclick="this.closest('.modal-overlay').classList.remove('open')">✕</button>
|
|
<h2>Настройка дашборда</h2>
|
|
<p style="color:var(--text-muted);font-size:13px;">Выберите показатели для отображения</p>
|
|
<div id="dash-settings-list"></div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-primary" onclick="saveDashSettings()">Сохранить</button>
|
|
<button type="button" class="btn btn-secondary" onclick="this.closest('.modal-overlay').classList.remove('open')">Отмена</button>
|
|
</div>
|
|
</div></div>
|
|
<script>
|
|
function getDashCards(){
|
|
return document.querySelectorAll('.card-grid > .card-link');
|
|
}
|
|
function loadDashSettings(){
|
|
var hidden = JSON.parse(localStorage.getItem('dash_hidden_cards') || '[]');
|
|
var cards = getDashCards();
|
|
var list = document.getElementById('dash-settings-list');
|
|
list.innerHTML = '';
|
|
cards.forEach(function(card){
|
|
var label = card.querySelector('.card-label')?.textContent || 'Показатель';
|
|
var key = label.trim();
|
|
var checked = hidden.indexOf(key) === -1;
|
|
var div = document.createElement('div');
|
|
div.style.cssText = 'padding:6px 0;';
|
|
div.innerHTML = '<label><input type="checkbox" class="dash-card-toggle" data-key="' + key.replace(/"/g,'"') + '" ' + (checked?'checked':'') + '> ' + key + '</label>';
|
|
list.appendChild(div);
|
|
});
|
|
}
|
|
function saveDashSettings(){
|
|
var hidden = [];
|
|
document.querySelectorAll('.dash-card-toggle').forEach(function(cb){
|
|
if(!cb.checked) hidden.push(cb.dataset.key);
|
|
});
|
|
localStorage.setItem('dash_hidden_cards', JSON.stringify(hidden));
|
|
applyDashSettings();
|
|
document.getElementById('modal-dash-settings').classList.remove('open');
|
|
}
|
|
function applyDashSettings(){
|
|
var hidden = JSON.parse(localStorage.getItem('dash_hidden_cards') || '[]');
|
|
var cards = getDashCards();
|
|
cards.forEach(function(card){
|
|
var label = card.querySelector('.card-label')?.textContent || '';
|
|
card.style.display = hidden.indexOf(label.trim()) !== -1 ? 'none' : '';
|
|
});
|
|
}
|
|
document.addEventListener('DOMContentLoaded', applyDashSettings);
|
|
</script>
|
|
<style>
|
|
.card-link { text-decoration:none; color:inherit; display:block; transition:transform 0.2s, box-shadow 0.2s; }
|
|
.card-link:hover { transform:translateY(-2px); box-shadow:var(--shadow); cursor:pointer; }
|
|
</style>
|
|
|
|
<div class="modal-overlay" id="changelog-modal"><div class="modal">
|
|
<button type="button" class="modal-close" onclick="closeChangelog()">✕</button>
|
|
<h2>История изменений</h2>
|
|
<div id="changelog-content" class="doc-content" style="padding:0;background:transparent;border:none;"></div>
|
|
</div></div>
|
|
|
|
<script>
|
|
function openChangelog(){
|
|
var modal = document.getElementById('changelog-modal');
|
|
modal.classList.add('open');
|
|
var content = document.getElementById('changelog-content');
|
|
if(!content.dataset.loaded){
|
|
content.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
|
fetch('/service/api/changelog').then(function(r){ return r.json() }).then(function(data){
|
|
content.innerHTML = data.html || '<p>Нет данных</p>';
|
|
content.dataset.loaded = '1';
|
|
});
|
|
}
|
|
}
|
|
function closeChangelog(){
|
|
document.getElementById('changelog-modal').classList.remove('open');
|
|
}
|
|
document.addEventListener('DOMContentLoaded', function(){
|
|
var nav = document.querySelector('.navbar__menu');
|
|
if(!nav) return;
|
|
var sidebarLinks = document.querySelectorAll('.sidebar-nav a[href]');
|
|
var saved = JSON.parse(localStorage.getItem('sidebar_favorites') || '[]');
|
|
sidebarLinks.forEach(function(link){
|
|
var star = document.createElement('span');
|
|
star.className = 'sidebar-star';
|
|
var url = link.getAttribute('href');
|
|
star.textContent = saved.indexOf(url) !== -1 ? '\u2605' : '\u2606';
|
|
star.style.cssText = 'cursor:pointer;margin-right:6px;font-size:14px;user-select:none;';
|
|
star.addEventListener('click', function(e){
|
|
e.preventDefault(); e.stopPropagation();
|
|
var favs = JSON.parse(localStorage.getItem('sidebar_favorites') || '[]');
|
|
var idx = favs.indexOf(url);
|
|
if(idx !== -1){ favs.splice(idx,1); star.textContent = '\u2606'; }
|
|
else { favs.push(url); star.textContent = '\u2605'; }
|
|
localStorage.setItem('sidebar_favorites', JSON.stringify(favs));
|
|
renderFavorites();
|
|
});
|
|
link.insertBefore(star, link.firstChild);
|
|
});
|
|
function renderFavorites(){
|
|
var existing = nav.querySelectorAll('.nav-fav');
|
|
existing.forEach(function(e){ e.remove(); });
|
|
var favs = JSON.parse(localStorage.getItem('sidebar_favorites') || '[]');
|
|
favs.forEach(function(url){
|
|
var orig = document.querySelector('.sidebar-nav a[href="' + url + '"]');
|
|
if(!orig) return;
|
|
var li = document.createElement('li');
|
|
li.className = 'nav-fav';
|
|
var a = document.createElement('a');
|
|
a.href = url; a.textContent = orig.textContent.replace(/^[☆★]/, '').trim();
|
|
li.appendChild(a);
|
|
nav.insertBefore(li, nav.lastElementChild);
|
|
});
|
|
}
|
|
renderFavorites();
|
|
// Restore sidebar scroll position
|
|
var sidebarNav = document.querySelector('.sidebar-nav');
|
|
var savedScroll = sessionStorage.getItem('sidebar_scroll');
|
|
if (sidebarNav && savedScroll) sidebarNav.scrollTop = parseInt(savedScroll, 10);
|
|
// Save scroll position before navigating
|
|
sidebarNav && sidebarNav.addEventListener('click', function(){
|
|
sessionStorage.setItem('sidebar_scroll', this.scrollTop);
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|