kpi-dashboard / templates /base.html
Admin
Initial HF deploy
04477e7
Raw
History Blame
26 kB
<!DOCTYPE html>
<html lang="ru" data-bs-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Columbus boutique — {% block title %}Платформа{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="icon" href="{{ url_for('static', filename='img/boutique-logo.webp') }}" type="image/webp">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='css/dashboard.css', v='20260707a') }}" rel="stylesheet">
{% block extra_head %}{% endblock %}
</head>
<body>
{% if not no_main_wrapper and current_user.is_authenticated %}
{# ===== DASHBOARD LAYOUT (authenticated users) ===== #}
<div class="d-layout">
<!-- Overlay for mobile -->
<div class="d-overlay" id="dOverlay"></div>
<!-- Sidebar -->
<aside class="d-sidebar" id="dSidebar">
<div class="d-sidebarg">
<a href="{{ url_for('main.main_page') }}" class="d-logo">
<div class="d-logo-img">CB</div>
<div>
<div class="d-logo-text">Columbus</div>
<div class="d-logo-sub">KPI Dashboard</div>
</div>
</a>
<nav class="d-nav">
<div class="d-nav-label">Навигация</div>
<a href="{{ url_for('main.main_page') }}" class="d-nav-item {% if request.endpoint == 'main.main_page' %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-grid-1x2-fill"></i></div><span class="d-nav-label-text">Главная</span>
</a>
<a href="{{ url_for('main.kpi_overview') }}" class="d-nav-item {% if request.endpoint and 'kpi_overview' in request.endpoint %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-bar-chart-fill"></i></div><span class="d-nav-label-text">KPI</span>
</a>
<a href="{{ url_for('main.ranking') }}" class="d-nav-item {% if request.endpoint and 'ranking' in request.endpoint %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-trophy-fill"></i></div><span class="d-nav-label-text">Ранкинг</span>
</a>
<a href="{{ url_for('main.employees_list') }}" class="d-nav-item {% if request.endpoint and ('employees' in request.endpoint or request.endpoint == 'main.employee') %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-people-fill"></i></div><span class="d-nav-label-text">Сотрудники</span>
</a>
<a href="{{ url_for('main.schedule') }}" class="d-nav-item {% if request.endpoint and 'schedule' in request.endpoint %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-calendar-week-fill"></i></div><span class="d-nav-label-text">График</span>
</a>
<a href="{{ url_for('admin.tasks') }}" class="d-nav-item {% if request.endpoint and 'tasks' in request.endpoint %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-check-circle-fill"></i></div><span class="d-nav-label-text">Задачи</span>
{% set task_count = tasks_count if tasks_count is defined else 0 %}
{% if task_count > 0 %}<span class="d-nav-badge">{{ task_count }}</span>{% endif %}
</a>
<a href="{{ url_for('admin.trainings') }}" class="d-nav-item {% if request.endpoint and 'trainings' in request.endpoint %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-book-fill"></i></div><span class="d-nav-label-text">Обучения</span>
</a>
<a href="{{ url_for('main.notifications') }}" class="d-nav-item {% if request.endpoint and 'notifications' in request.endpoint %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-bell-fill"></i></div><span class="d-nav-label-text">Уведомления</span>
{% if unread_notifications_count is defined and unread_notifications_count > 0 %}
<span class="d-nav-badge" style="background:var(--red);">{{ unread_notifications_count }}</span>
{% endif %}
</a>
{% if current_user.can_edit() %}
<div class="d-sidebar-divider"></div>
<div class="d-nav-label">Администрирование</div>
<a href="{{ url_for('admin.admin_dashboard') }}" class="d-nav-item {% if request.endpoint and 'dashboard' in request.endpoint %}active{% endif %}">
<span class="d-nav-iconbox"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg></span>
<span class="d-nav-text">Дашборд</span>
</a>
<a href="{{ url_for('admin.users') }}" class="d-nav-item {% if request.endpoint and 'users' in request.endpoint %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-person-gear"></i></div><span class="d-nav-label-text">Пользователи</span>
</a>
<a href="{{ url_for('admin.kpi_tools') }}" class="d-nav-item {% if request.endpoint and 'kpi_tools' in request.endpoint %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-tools"></i></div><span class="d-nav-label-text">Управление KPI</span>
</a>
<a href="{{ url_for('main.kpi_test') }}" class="d-nav-item {% if request.endpoint and 'kpi_test' in request.endpoint %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-table"></i></div><span class="d-nav-label-text">KPI Тест</span>
</a>
<a href="{{ url_for('admin.upload_file') }}" class="d-nav-item {% if request.endpoint and 'upload' in request.endpoint %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-upload"></i></div><span class="d-nav-label-text">Загрузка файлов</span>
</a>
<a href="{{ url_for('admin.audits_list') }}" class="d-nav-item {% if request.endpoint and 'audit' in request.endpoint %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-shield-check"></i></div><span class="d-nav-label-text">Аудиты</span>
</a>
<a href="{{ url_for('main.kpi_sync_test') }}" class="d-nav-item {% if request.endpoint and 'kpi_sync_test' in request.endpoint %}active{% endif %}">
<div class="d-nav-iconbox"><i class="bi bi-arrow-repeat"></i></div><span class="d-nav-label-text">Sync Test</span>
</a>
{% endif %}
</nav>
<div class="d-sidebar-footer">
<button class="d-theme-btn" id="dThemeToggle" title="Тема">
<i class="bi bi-moon-stars-fill"></i>
<span id="dThemeLabel">Тёмная</span>
</button>
<button class="d-theme-btn" id="dSidebarCollapse" title="Свернуть боковую панель" style="flex:0;padding:8px;">
<i class="bi bi-chevron-left"></i>
</button>
</div>
</div>
</aside>
<!-- Main Area -->
<div class="d-main" id="dMain">
<!-- Top Bar -->
<header class="d-topbar">
<div class="d-topbar-left">
<button class="d-sidebar-toggle" id="dSidebarToggle" aria-label="Меню">
<i class="bi bi-list"></i>
</button>
<div class="d-page-title">{% block page_title %}Панель управления{% endblock %}</div>
</div>
<div class="d-topbar-right">
<!-- Notifications dropdown -->
<div class="dropdown">
<button class="d-topbar-btn" data-bs-toggle="dropdown" aria-label="Уведомления">
<i class="bi bi-bell"></i>
{% if unread_notifications_count is defined and unread_notifications_count > 0 %}
<span class="d-topbar-badge"></span>
{% endif %}
</button>
<div class="dropdown-menu dropdown-menu-end dropdown-apple" style="width:340px;">
<div class="d-flex justify-content-between align-items-center px-3 py-2 border-bottom" style="border-color:var(--card-border)!important;">
<strong style="font-size:14px;font-weight:600;">Уведомления</strong>
<a href="{{ url_for('main.notifications') }}" style="font-size:13px;color:var(--accent);text-decoration:none;">Все</a>
</div>
<div id="notif-list">
<div class="text-center py-3" style="color:var(--text-secondary);font-size:13px;">Загрузка...</div>
</div>
</div>
</div>
<!-- User dropdown -->
<div class="dropdown">
<a href="#" class="d-user-dropdown" data-bs-toggle="dropdown" id="dUserDropdown">
<div class="d-user-avatar">{{ current_user.full_name[:1] }}</div>
<span class="d-user-name">{{ current_user.full_name.split()[-1] }}</span>
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-apple">
<div class="px-3 py-2 border-bottom" style="border-color:var(--card-border)!important;">
<div style="font-weight:600;font-size:14px;">{{ current_user.full_name }}</div>
<div style="font-size:12px;color:var(--text-secondary);">{{ {'admin':'Админ','junior_expert':'Младший эксперт','sm':'Руководитель','senior_expert':'Старший эксперт','employee':'Эксперт','guest':'Гость'}.get(current_user.role, current_user.role) }}</div>
</div>
<a class="dropdown-item" href="{{ url_for('main.ranking') }}"><i class="bi bi-trophy me-2"></i>Ранкинг</a>
<a class="dropdown-item" href="{{ url_for('auth.profile') }}"><i class="bi bi-person-circle me-2"></i>Профиль</a>
<div class="dropdown-divider"></div>
<button class="dropdown-item" id="dThemeToggleDropdown"><i class="bi bi-moon-stars-fill me-2"></i><span>Тёмная тема</span></button>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{{ url_for('auth.logout') }}"><i class="bi bi-box-arrow-right me-2"></i>Выйти</a>
</div>
</div>
</div>
</header>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div style="padding:16px 24px 0;">
{% for category, message in messages %}
<div class="alert-apple {{ category }} mb-2">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<!-- Breadcrumbs -->
{% if breadcrumbs %}
<div class="d-breadcrumbs" style="padding:16px 24px 0;">
<a href="{{ url_for('main.main_page') }}"><i class="bi bi-house-door-fill"></i></a>
{% for b in breadcrumbs %}
<span class="sep">/</span>
{% if b.url %}<a href="{{ b.url }}">{{ b.label }}</a>{% else %}<span>{{ b.label }}</span>{% endif %}
{% endfor %}
</div>
{% endif %}
<!-- Content -->
<div class="d-content">
{% block content %}{% endblock %}
</div>
<!-- Footer -->
<footer class="cb-footer" style="margin-top:0;">
<div class="cb-footer-inner">
<div class="cb-footer-bottom">
<span>&copy; 2026 Columbus boutique. Все права защищены.</span>
</div>
</div>
</footer>
</div>
</div>
{% else %}
{# ===== LOGIN / LANDING LAYOUT (no sidebar) ===== #}
<!-- CB Topbar -->
<div class="cb-topbar" id="cbTopbar">
<div class="cb-topbar-inner">
<a href="{{ url_for('main.main_page') }}" class="cb-logo">
<img class="cb-logo-img" src="{{ url_for('static', filename='img/boutique-logo.webp') }}" alt="CB" onerror="this.parentElement.style.display='none'">
COLUMBUS BOUTIQUE
</a>
<div class="cb-nav" id="cbNav">
<a href="{{ url_for('main.main_page') }}" class="{% if request.endpoint == 'main.main_page' %}cb-active{% endif %}">Главная</a>
<a href="{{ url_for('main.schedule') }}" class="{% if request.endpoint == 'main.schedule' %}cb-active{% endif %}">График</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('admin.tasks') }}" class="{% if request.endpoint and 'tasks' in request.endpoint %}cb-active{% endif %}">Задачи</a>
{% endif %}
</div>
<div class="cb-topbar-right">
{% if current_user.is_authenticated %}
<div class="dropdown">
<a href="#" data-bs-toggle="dropdown" class="cb-user-dropdown" id="userDropdown">
<div class="cb-avatar">{{ current_user.full_name[:1] }}</div>
<span class="cb-user-name d-none d-md-inline">{{ current_user.full_name.split()[-1] }}</span>
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-apple">
<div class="px-3 py-2 border-bottom" style="border-color:rgba(255,255,255,0.05)!important;">
<div style="font-weight:600;font-size:14px;">{{ current_user.full_name }}</div>
<div style="font-size:12px;color:#A0A5B5;">{{ {'admin':'Админ','junior_expert':'Младший эксперт','sm':'Руководитель','senior_expert':'Старший эксперт','employee':'Эксперт','guest':'Гость'}.get(current_user.role, current_user.role) }}</div>
</div>
<a class="dropdown-item" href="{{ url_for('main.ranking') }}"><i class="bi bi-trophy me-2"></i>Ранкинг</a>
<a class="dropdown-item" href="{{ url_for('auth.profile') }}"><i class="bi bi-person-circle me-2"></i>Профиль</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="theme-toggle-legacy"><i class="bi bi-moon-stars-fill me-2"></i><span>Тёмная тема</span></a>
<a class="dropdown-item" href="{{ url_for('auth.logout') }}"><i class="bi bi-box-arrow-right me-2"></i>Выйти</a>
</div>
</div>
{% endif %}
<button class="cb-hamburger" id="cbMenuToggle" aria-label="Меню">
<svg viewBox="0 0 96 96" width="24" height="24"><path d="M13,57V52h70v5Zm0-26V26h70v5Zm0-26V0h70v5Z"/></svg>
</button>
</div>
</div>
</div>
<!-- Mobile Menu -->
<div class="cb-mobile-menu" id="cbMobileMenu">
<div class="cb-mobile-header">
<span style="font-weight:700;font-size:16px;">Меню</span>
<button class="cb-mobile-close" id="cbMenuClose">&times;</button>
</div>
<div class="cb-mobile-nav">
<a href="{{ url_for('main.main_page') }}" class="cb-mobile-link">Главная</a>
<a href="{{ url_for('main.schedule') }}" class="cb-mobile-link">График</a>
{% if current_user.is_authenticated %}
<a href="{{ url_for('main.kpi_overview') }}" class="cb-mobile-link">KPI</a>
<a href="{{ url_for('main.ranking') }}" class="cb-mobile-link">Ранкинг</a>
<a href="{{ url_for('main.employees_list') }}" class="cb-mobile-link">Сотрудники</a>
<a href="{{ url_for('admin.tasks') }}" class="cb-mobile-link">Задачи</a>
<a href="{{ url_for('admin.trainings') }}" class="cb-mobile-link">Обучения</a>
<a href="{{ url_for('main.notifications') }}" class="cb-mobile-link">
{% if unread_notifications_count > 0 %}
<span style="font-size:10px;font-weight:600;padding:1px 7px;border-radius:999px;background:#0075FF;color:#fff;margin-right:3px;">{{ unread_notifications_count }}</span>
{% endif %}
Уведомления
</a>
{% if current_user.can_edit() %}
<div class="cb-mobile-divider"></div>
<a href="{{ url_for('admin.users') }}" class="cb-mobile-link">👤 Пользователи</a>
<a href="{{ url_for('admin.upload_file') }}" class="cb-mobile-link">📤 Загрузка файлов</a>
<a href="{{ url_for('admin.kpi_tools') }}" class="cb-mobile-link">🔧 Управление KPI</a>
<a href="{{ url_for('admin.audits_list') }}" class="cb-mobile-link">🛡 Аудиты</a>
<a href="{{ url_for('main.kpi_sync_test') }}" class="cb-mobile-link">🔄 Sync Test</a>
{% endif %}
<div class="cb-mobile-divider"></div>
<a href="{{ url_for('auth.profile') }}" class="cb-mobile-link">Профиль</a>
<a href="{{ url_for('auth.logout') }}" class="cb-mobile-link" style="color:#dc2626;">Выйти</a>
{% else %}
<div class="cb-mobile-divider"></div>
<a href="{{ url_for('auth.login') }}" class="btn-login" style="width:100%;justify-content:center;padding:12px;text-decoration:none;margin-top:8px;">Войти</a>
{% endif %}
</div>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flash-wrap">
{% for category, message in messages %}
<div class="alert-apple {{ category }}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% if breadcrumbs %}
<div style="padding:12px 24px 0;font-size:12px;color:#A0A5B5;">
{% for b in breadcrumbs %}
{% if b.url %}<a href="{{ b.url }}" style="color:#A0A5B5;text-decoration:none;">{{ b.label }}</a>{% else %}<span>{{ b.label }}</span>{% endif %}
{% if not loop.last %}<span style="margin:0 6px;"></span>{% endif %}
{% endfor %}
</div>
{% endif %}
{% block content_full %}{% endblock %}
{% if not no_main_wrapper %}<div class="main-content" id="mainContent">{% endif %}
{% block login_content %}{% endblock %}
{% if not no_main_wrapper %}</div>{% endif %}
<!-- Footer (landing/login) -->
<footer class="cb-footer">
<div class="cb-footer-inner">
<div class="cb-footer-bottom">
<span>&copy; 2026 Columbus boutique. Все права защищены.</span>
</div>
</div>
</footer>
{% endif %}
{# Override in child templates if custom footer needed #}
{% block footer %}{% endblock %}
<!-- PDF Viewer Modal -->
<div class="modal fade" id="pdfViewerModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content" style="border-radius:14px;overflow:hidden;">
<div class="modal-header" style="border:none;padding:12px 16px;">
<span style="font-weight:600;font-size:14px;" id="pdfViewerTitle">Просмотр файла</span>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" style="padding:0 16px 16px;height:80vh;">
<iframe id="pdfViewerIframe" src="" style="width:100%;height:100%;border:none;border-radius:8px;"></iframe>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// ===== THEME =====
function getTheme() { return document.documentElement.getAttribute('data-bs-theme') || 'dark'; }
function setTheme(t) {
document.documentElement.setAttribute('data-bs-theme', t);
localStorage.setItem('cb-theme', t);
var icons = document.querySelectorAll('#dThemeToggle i, #dThemeToggleDropdown i');
var labels = document.querySelectorAll('#dThemeLabel, #dThemeToggleDropdown span');
var isDark = t === 'dark';
icons.forEach(function(el) { el.className = isDark ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill'; });
labels.forEach(function(el) { if (el.textContent.trim() === 'Тёмная тема' || el.textContent.trim() === 'Светлая тема') el.textContent = isDark ? 'Тёмная тема' : 'Светлая тема'; });
}
var savedTheme = localStorage.getItem('cb-theme') || 'dark';
setTheme(savedTheme);
document.querySelectorAll('#dThemeToggle, #dThemeToggleDropdown, #theme-toggle-legacy').forEach(function(el) {
if (el) el.addEventListener('click', function(e) { e.preventDefault(); setTheme(getTheme() === 'dark' ? 'light' : 'dark'); });
});
// ===== SIDEBAR TOGGLE =====
var sidebar = document.getElementById('dSidebar');
var overlay = document.getElementById('dOverlay');
var toggle = document.getElementById('dSidebarToggle');
var collapseBtn = document.getElementById('dSidebarCollapse');
function isMobile() { return window.innerWidth <= 768; }
function toggleSidebar() {
if (isMobile()) {
sidebar.classList.toggle('open');
if (overlay) overlay.classList.toggle('show');
} else {
sidebar.classList.toggle('hidden');
var isHidden = sidebar.classList.contains('hidden');
localStorage.setItem('d-sidebar-hidden', isHidden ? '1' : '0');
}
}
function collapseSidebar() {
if (isMobile()) return;
sidebar.classList.remove('hidden');
sidebar.classList.toggle('collapsed');
var isCollapsed = sidebar.classList.contains('collapsed');
localStorage.setItem('d-sidebar-collapsed', isCollapsed ? '1' : '0');
if (collapseBtn) collapseBtn.innerHTML = isCollapsed ? '<i class=\'bi bi-chevron-right\'></i>' : '<i class=\'bi bi-chevron-left\'></i>';
}
if (toggle) toggle.addEventListener('click', toggleSidebar);
if (collapseBtn) collapseBtn.addEventListener('click', collapseSidebar);
if (overlay) {
overlay.addEventListener('click', function() {
sidebar.classList.remove('open');
overlay.classList.remove('show');
});
}
var savedHidden = localStorage.getItem('d-sidebar-hidden');
if (savedHidden === '1' && !isMobile()) {
sidebar.classList.add('hidden');
}
var savedCollapsed = localStorage.getItem('d-sidebar-collapsed');
if (savedCollapsed === '1' && !isMobile()) {
sidebar.classList.add('collapsed');
if (collapseBtn) collapseBtn.innerHTML = '<i class=\'bi bi-chevron-right\'></i>';
}
// ===== MOBILE MENU (legacy) =====
var cbToggle = document.getElementById('cbMenuToggle');
var cbMenu = document.getElementById('cbMobileMenu');
var cbClose = document.getElementById('cbMenuClose');
if (cbToggle && cbMenu) {
cbToggle.addEventListener('click', function() { cbMenu.classList.add('open'); });
cbClose.addEventListener('click', function() { cbMenu.classList.remove('open'); });
cbMenu.addEventListener('click', function(e) { if (e.target === cbMenu) cbMenu.classList.remove('open'); });
}
});
function openPdfViewer(url, title) {
document.getElementById('pdfViewerTitle').textContent = title || 'Просмотр файла';
document.getElementById('pdfViewerIframe').src = url;
var modal = new bootstrap.Modal(document.getElementById('pdfViewerModal'));
modal.show();
}
document.addEventListener('hidden.bs.modal', function(e) {
if (e.target.id === 'pdfViewerModal') {
document.getElementById('pdfViewerIframe').src = '';
}
});
</script>
{% block extra_js %}{% endblock %}
</body>
</html>