Files
ACORE/templates/index.html
T
lokses fe463c11af chat: вызывать chatInit после HTMX-перехода
Без этого список каналов зависал на «Загрузка…» при открытии чата из меню.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 12:45:34 +03:00

920 lines
54 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% load common_tags %}
{% load static %}
{% load i18n %}
<!DOCTYPE html>
<html lang="{{ LANGUAGE_CODE }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>АКС — Корпоративний Портал</title>
<link rel="stylesheet" href="{% static 'assets/css/main.css' %}">
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script>htmx.config.allowEval = false;</script>
<script src="{% static 'tinymce/tinymce.min.js' %}"></script>
<script>
// HTMX собирает данные формы ДО configRequest — дописываем актуальное
// содержимое редакторов прямо в параметры запроса
document.addEventListener('htmx:configRequest', function(evt) {
if (!window.tinymce || !evt.detail || !evt.detail.parameters) return;
tinymce.get().forEach(function(editor) {
var el = editor.getElement();
if (!el || !el.name) return;
var params = evt.detail.parameters;
if (typeof params.set === 'function') {
params.set(el.name, editor.getContent());
} else {
params[el.name] = editor.getContent();
}
});
});
</script>
<script>(function(){var t=localStorage.getItem('theme')||'light';if(t!=='light')document.documentElement.setAttribute('data-theme',t);})();</script>
<script>window.ACORE_MAP_CONFIG={cartoApiKey:"{{ carto_basemaps_api_key|escapejs }}"};</script>
<script src="{% url 'javascript-catalog' %}"></script>
<script src="{% static 'assets/js/map-tiles.js' %}"></script>
<script src="{% static 'assets/js/pagination.js' %}"></script>
<script src="{% static 'assets/js/pharmacies.js' %}"></script>
<script src="{% static 'assets/js/employees.js' %}"></script>
<script src="{% static 'assets/js/timetrack.js' %}"></script>
<script src="{% static 'assets/js/utilities.js' %}"></script>
<script src="{% static 'assets/js/people-picker.js' %}"></script>
<script src="{% static 'assets/js/searchable-select.js' %}"></script>
<script src="{% static 'assets/js/hr-portal.js' %}"></script>
<script src="{% static 'assets/js/calendar.js' %}"></script>
<script src="{% static 'assets/js/employee-profile.js' %}"></script>
<script src="{% static 'assets/js/it-manager.js' %}"></script>
<script src="{% static 'assets/js/reports-template-editor.js' %}"></script>
<script src="{% static 'assets/js/directory-quick-add.js' %}"></script>
<script src="{% static 'assets/js/directories.js' %}"></script>
<script src="{% static 'assets/js/user-groups.js' %}"></script>
<script src="{% static 'assets/js/chat.js' %}"></script>
<script src="{% static 'assets/js/documents.js' %}"></script>
</head>
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
<script>
function isMobile() { return window.innerWidth < 768; }
function toggleSidebar() {
const s = document.getElementById('sidebar');
if (isMobile()) {
const backdrop = document.getElementById('sidebar-backdrop');
const opening = !s.classList.contains('mobile-open');
s.classList.toggle('mobile-open');
backdrop.classList.toggle('active');
document.body.style.overflow = opening ? 'hidden' : '';
} else {
s.classList.toggle('collapsed');
}
}
function closeSidebar() {
const s = document.getElementById('sidebar');
const backdrop = document.getElementById('sidebar-backdrop');
s.classList.remove('mobile-open');
backdrop.classList.remove('active');
document.body.style.overflow = '';
}
function setActive(el) {
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
el.classList.add('active');
const hxGet = el.getAttribute('hx-get') || '';
syncBottomNav(hxGet);
if (isMobile()) closeSidebar();
}
function syncBottomNav(hxGet) {
document.querySelectorAll('.bottom-nav-item[hx-get]').forEach(i => {
i.classList.toggle('active', i.getAttribute('hx-get') === hxGet);
});
}
function bottomNavClick(el) {
document.querySelectorAll('.bottom-nav-item').forEach(i => i.classList.remove('active'));
el.classList.add('active');
const hxGet = el.getAttribute('hx-get') || '';
const sidebarItem = document.querySelector(`.nav-item[hx-get="${hxGet}"]`);
if (sidebarItem) {
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
sidebarItem.classList.add('active');
}
}
function toggleUserMenu() {
const d = document.getElementById('user-dropdown');
d.style.display = d.style.display === 'none' ? 'block' : 'none';
}
function closeUserMenu() {
document.getElementById('user-dropdown').style.display = 'none';
}
function applyTheme(theme) {
const html = document.documentElement;
if (theme === 'light') {
html.removeAttribute('data-theme');
} else {
html.setAttribute('data-theme', theme);
}
localStorage.setItem('theme', theme);
document.querySelectorAll('.theme-option').forEach(function(btn) {
btn.classList.toggle('active', btn.getAttribute('data-theme-value') === theme);
});
}
function toggleThemeSwitcher() {
const d = document.getElementById('theme-dropdown');
const isOpen = d.style.display !== 'none';
if (!isOpen) closeUserMenu();
d.style.display = isOpen ? 'none' : 'block';
}
function closeThemeSwitcher() {
const d = document.getElementById('theme-dropdown');
if (d) d.style.display = 'none';
}
function handleBgFile(input) {
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
const dataUrl = e.target.result;
const opacity = parseFloat(localStorage.getItem('bg-opacity') || '0.88');
try { localStorage.setItem('bg-image', dataUrl); } catch(err) { /* image too large to persist */ }
applyBgImage(dataUrl, opacity);
};
reader.readAsDataURL(file);
}
function applyBgImage(url, opacity) {
const wp = document.getElementById('bg-wallpaper');
const ov = document.getElementById('bg-overlay');
wp.style.backgroundImage = 'url("' + url + '")';
wp.style.display = 'block';
ov.style.display = 'block';
ov.style.opacity = opacity;
document.getElementById('main-content').style.background = 'transparent';
document.body.style.background = 'transparent';
const slider = document.getElementById('bg-opacity-slider');
if (slider) slider.value = Math.round(opacity * 100);
const opRow = document.getElementById('bg-opacity-row');
if (opRow) opRow.style.display = 'flex';
const clearBtn = document.getElementById('bg-clear-btn');
if (clearBtn) clearBtn.style.display = 'flex';
}
function clearBgImage() {
document.getElementById('bg-wallpaper').style.display = 'none';
document.getElementById('bg-overlay').style.display = 'none';
document.getElementById('main-content').style.background = '';
document.body.style.background = '';
localStorage.removeItem('bg-image');
const opRow = document.getElementById('bg-opacity-row');
if (opRow) opRow.style.display = 'none';
const clearBtn = document.getElementById('bg-clear-btn');
if (clearBtn) clearBtn.style.display = 'none';
const fi = document.getElementById('bg-file-input');
if (fi) fi.value = '';
}
function setBgOpacity(val) {
const opacity = val / 100;
const ov = document.getElementById('bg-overlay');
if (ov) ov.style.opacity = opacity;
localStorage.setItem('bg-opacity', opacity);
}
document.addEventListener('click', function(e) {
const userWrapper = document.getElementById('user-menu-wrapper');
if (userWrapper && !userWrapper.contains(e.target)) closeUserMenu();
const themeWrapper = document.getElementById('theme-switcher-wrapper');
if (themeWrapper && !themeWrapper.contains(e.target)) closeThemeSwitcher();
});
window.addEventListener('resize', function() {
if (!isMobile()) closeSidebar();
});
</script>
<!-- Background wallpaper layers -->
<div id="bg-wallpaper" style="display:none; position:fixed; inset:0; z-index:-2; background-size:cover; background-position:center; background-repeat:no-repeat;"></div>
<div id="bg-overlay" style="display:none; position:fixed; inset:0; z-index:-1; background-color:var(--color-background); transition:opacity 200ms;"></div>
<!-- Loader bar (HTMX) -->
<div class="loader-bar" id="loader" aria-hidden="true"></div>
<div style="display:flex; height:100dvh; overflow:hidden;">
<!-- Backdrop (mobile overlay) -->
<div id="sidebar-backdrop" class="sidebar-backdrop" onclick="closeSidebar()"></div>
<!-- ══════════════════ SIDEBAR ══════════════════ -->
<aside id="sidebar" class="sidebar">
<!-- Шапка -->
<div class="sidebar-header">
<div class="sidebar-logo">
<img src="{% static 'assets/img/logosm.svg' %}" alt="АКС" style="width:100%; height:100%; object-fit:contain;">
</div>
<div class="sidebar-logo-text">
<div style="color:var(--color-sidebar-accent-foreground); font-size:0.8125rem; font-weight:700; line-height:1.2; letter-spacing:0.01em;">{% trans "Сеть аптек" %}</div>
<div style="color:var(--color-sidebar-foreground); font-size:0.625rem; opacity:0.6;">{% trans "Портал пользователя" %}</div>
</div>
</div>
<!-- Навигация -->
<nav style="flex:1; overflow-y:auto; overflow-x:hidden; padding:0.5rem; display:flex; flex-direction:column; gap:1rem;">
<!-- Головне меню -->
<div>
<div class="nav-section-label">{% trans "Главное меню" %}</div>
<div style="display:flex; flex-direction:column; gap:2px;">
<a href="/dashboard/" class="nav-item"
hx-get="/dashboard/" hx-target="#main-content" hx-push-url="/dashboard/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>
</svg>
<span class="nav-label-text">{% trans "Главная" %}</span>
</a>
{% if request.user|can_section:'news' %}
<a href="/news/" class="nav-item"
hx-get="/news/" hx-target="#main-content" hx-push-url="/news/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/>
<path d="M18 14h-8M15 18h-5M10 6h8v4h-8V6Z"/>
</svg>
<span class="nav-label-text">{% trans "Новости" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'hr_portal' %}
<a href="/hr-portal/" class="nav-item"
hx-get="/hr-portal/" hx-target="#main-content" hx-push-url="/hr-portal/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span class="nav-label-text">{% trans "HR портал" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'tasks' %}
<a href="/tasks/" class="nav-item"
hx-get="/tasks/" hx-target="#main-content" hx-push-url="/tasks/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
<path d="m9 14 2 2 4-4"/>
</svg>
<span class="nav-label-text">{% trans "Задачи" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'dislocation' %}
<a href="/dislocation/" class="nav-item"
hx-get="/dislocation/" hx-target="#main-content" hx-push-url="/dislocation/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/>
</svg>
<span class="nav-label-text">{% trans "Дислокация" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'documents' %}
<a href="/documents/" class="nav-item"
hx-get="/documents/" hx-target="#main-content" hx-push-url="/documents/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/>
<path d="M14 2v4a2 2 0 0 0 2 2h4M10 9H8M16 13H8M16 17H8"/>
</svg>
<span class="nav-label-text">{% trans "Документы" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'email' %}
<a href="/email/" class="nav-item"
hx-get="/email/" hx-target="#main-content" hx-push-url="/email/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect width="20" height="16" x="2" y="4" rx="2"/>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>
</svg>
<span class="nav-label-text">{% trans "Электронная почта" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'chat' %}
<a href="/chat/" class="nav-item"
hx-get="/chat/" hx-target="#main-content" hx-push-url="/chat/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/>
</svg>
<span class="nav-label-text">{% trans "Чат" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'calendar' %}
<a href="/calendar/" class="nav-item"
hx-get="/calendar/" hx-target="#main-content" hx-push-url="/calendar/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect width="18" height="18" x="3" y="4" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/>
</svg>
<span class="nav-label-text">{% trans "Календарь" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'databases' %}
<a href="/databases/" class="nav-item"
hx-get="/databases/" hx-target="#main-content" hx-push-url="/databases/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
</svg>
<span class="nav-label-text">{% trans "Системные справочники" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'reports' %}
<a href="/reports/" class="nav-item"
hx-get="/reports/" hx-target="#main-content" hx-push-url="/reports/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 3v18h18"/><path d="m19 9-5 5-4-4-3 3"/>
</svg>
<span class="nav-label-text">{% trans "Отчёты" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'purchases' %}
<a href="/purchases/" class="nav-item"
hx-get="/purchases/" hx-target="#main-content" hx-push-url="/purchases/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/>
<path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/>
</svg>
<span class="nav-label-text">{% trans "Закупки" %}</span>
</a>
{% endif %}
{% if request.user|can_accounting %}
<a href="/accounting/" class="nav-item"
hx-get="/accounting/" hx-target="#main-content" hx-push-url="/accounting/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/>
<path d="M7 7h.01M7 11h4M13 11h4M7 15h10"/>
</svg>
<span class="nav-label-text">{% trans "Учётные данные" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'settings' %}
<a href="/settings/" class="nav-item"
hx-get="/settings/" hx-target="#main-content" hx-push-url="/settings/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<span class="nav-label-text">{% trans "Настройки" %}</span>
</a>
{% endif %}
</div>
</div>
<!-- Управління -->
{% if request.user|can_section:'structure' or request.user|can_section:'user_groups' or request.user|can_section:'pharmacies' %}
<div>
<div class="nav-section-label">{% trans "Управление" %}</div>
<div style="display:flex; flex-direction:column; gap:2px;">
{% if request.user|can_section:'structure' %}
<a href="/structure/" class="nav-item"
hx-get="/structure/" hx-target="#main-content" hx-push-url="/structure/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M2 4h12M2 8h8M2 12h5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<span class="nav-label-text">{% trans "Структура" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'user_groups' %}
<a href="/user-groups/" class="nav-item"
hx-get="/user-groups/" hx-target="#main-content" hx-push-url="/user-groups/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span class="nav-label-text">{% trans "Группы пользователей" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'pharmacies' %}
<a href="/pharmacies/" class="nav-item"
hx-get="/pharmacies/" hx-target="#main-content" hx-push-url="/pharmacies/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14.106 5.553a2 2 0 0 0 1.788 0l3.659-1.83A1 1 0 0 1 21 4.619v12.764a1 1 0 0 1-.553.894l-4.553 2.277a2 2 0 0 1-1.788 0l-4.212-2.106a2 2 0 0 0-1.788 0l-3.659 1.83A1 1 0 0 1 3 19.381V6.618a1 1 0 0 1 .553-.894l4.553-2.277a2 2 0 0 1 1.788 0z"/>
<path d="M15 5.764v15"/><path d="M9 3.236v15"/>
</svg>
<span class="nav-label-text">{% trans "Аптеки" %}</span>
</a>
{% endif %}
</div>
</div>
{% endif %}
<!-- Підтримка -->
<div>
<div class="nav-section-label">{% trans "Поддержка" %}</div>
<div style="display:flex; flex-direction:column; gap:2px;">
{% if request.user|can_section:'help' %}
<a href="/help/" class="nav-item"
hx-get="/help/" hx-target="#main-content" hx-push-url="/help/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>
</svg>
<span class="nav-label-text">{% trans "Справка" %}</span>
</a>
{% endif %}
<a href="/it-support/" class="nav-item"
hx-get="/it-support/" hx-target="#main-content" hx-push-url="/it-support/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 18v-6a9 9 0 0 1 18 0v6"/>
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/>
</svg>
<span class="nav-label-text">{% trans "Поддержка IT" %}</span>
</a>
{% if request.user|can_section:'it_manager' %}
<a href="/it-manager/" class="nav-item"
hx-get="/it-manager/" hx-target="#main-content" hx-push-url="/it-manager/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/>
</svg>
<span class="nav-label-text">{% trans "IT-отдел" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'ad_mapping' %}
<a href="/ad-mapping/" class="nav-item"
hx-get="/ad-mapping/" hx-target="#main-content" hx-push-url="/ad-mapping/"
hx-indicator="#loader" onclick="setActive(this)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect width="8" height="8" x="2" y="2" rx="2"/><rect width="8" height="8" x="14" y="14" rx="2"/>
<path d="M10 6h4a2 2 0 0 1 2 2v2M14 18h-4a2 2 0 0 1-2-2v-2"/>
</svg>
<span class="nav-label-text">{% trans "Active Directory" %}</span>
</a>
{% endif %}
</div>
</div>
</nav>
<!-- Футер сайдбара -->
<div class="sidebar-footer" style="justify-content:center; padding:0.75rem;">
<span class="sidebar-logo-text" style="font-size:0.625rem; color:var(--color-sidebar-foreground); opacity:0.4; white-space:nowrap;">© 2026 АКС Корпоративний Портал | v1.0</span>
</div>
</aside>
<!-- ══════════════════ MAIN ══════════════════ -->
<div style="flex:1; display:flex; flex-direction:column; min-width:0; height:100vh;">
<!-- Header -->
<header style="height:56px; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--color-border); background:var(--color-card); padding:0 1.25rem; flex-shrink:0; gap:1rem;">
<div style="display:flex; align-items:center; gap:0.75rem; flex:1;">
<!-- Кнопка сворачивания -->
<button onclick="toggleSidebar()" class="btn btn-ghost btn-icon" title="Згорнути меню">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/>
</svg>
</button>
<!-- Поиск -->
<div class="header-search" id="header-search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="header-search-icon">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
</svg>
<input type="search"
id="portal-search-input"
name="q"
placeholder="{% trans 'Поиск...' %}"
class="header-search-input"
autocomplete="off"
enterkeyhint="search"
hx-get="/api/search"
hx-trigger="keyup changed delay:280ms, search, focus"
hx-target="#search-results"
hx-swap="innerHTML"
aria-autocomplete="list"
aria-controls="search-results"
aria-expanded="false">
<div id="search-results" class="portal-search-results" hidden></div>
</div>
</div>
<div style="display:flex; align-items:center; gap:0.5rem;">
{% if dev_access_preview_enabled %}
<!-- Dev: смотреть как роль -->
<form method="post" action="{% url 'access_preview' %}" id="dev-access-preview-form" style="margin:0;">
{% csrf_token %}
<input type="hidden" name="next" value="">
<select name="preview" title="{% trans 'Смотреть как…' %}"
onchange="this.form.next.value = window.location.pathname + window.location.search; this.form.submit();"
class="input"
style="height:2rem; padding:0 0.5rem; font-size:0.75rem; width:auto; max-width:12rem; cursor:pointer;{% if dev_access_preview_active %} border-color:#d97706; background:color-mix(in srgb, #f59e0b 14%, var(--color-card));{% endif %}">
{% for preset in dev_access_preview_presets %}
<option value="{{ preset.id }}" {% if preset.id == dev_access_preview %}selected{% endif %}>{{ preset.label }}</option>
{% endfor %}
</select>
</form>
{% endif %}
<!-- Language switcher -->
{% get_current_language as CUR_LANG %}
<form action="{% url 'set_language' %}" method="post" style="margin:0;">
{% csrf_token %}
<input type="hidden" name="next" value="/">
<select name="language" onchange="this.form.next.value = window.location.pathname; this.form.submit();" title="{% trans 'Язык' %}"
class="input" style="height:2rem; padding:0 0.5rem; font-size:0.75rem; width:auto; cursor:pointer;">
{% for code, name in LANGUAGES %}
<option value="{{ code }}" {% if code == CUR_LANG %}selected{% endif %}>{{ name }}</option>
{% endfor %}
</select>
</form>
<!-- Theme switcher -->
<div style="position:relative;" id="theme-switcher-wrapper">
<button onclick="toggleThemeSwitcher()" class="btn btn-ghost btn-icon" title="Тема інтерфейсу">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M12 2a10 10 0 0 1 0 20V2z" fill="currentColor" stroke="none"/>
</svg>
</button>
<div id="theme-dropdown" style="display:none; position:absolute; top:calc(100% + 8px); right:0; background:var(--color-card); border:1px solid var(--color-border); border-radius:var(--radius); box-shadow:0 8px 24px rgba(0,0,0,0.15); z-index:1000; padding:0.375rem; min-width:160px;">
<div style="padding:0.25rem 0.625rem 0.375rem; font-size:0.625rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em;">Тема</div>
<button class="theme-option" data-theme-value="light" onclick="applyTheme('light')">
<span class="theme-dot" style="background:hsl(224.8,81%,60.8%);"></span>
Light Blue
<span class="theme-check">✓</span>
</button>
<button class="theme-option" data-theme-value="dark" onclick="applyTheme('dark')">
<span class="theme-dot" style="background:hsl(215,25%,10%); border-color:hsl(215,25%,30%);"></span>
Dark
<span class="theme-check">✓</span>
</button>
<button class="theme-option" data-theme-value="slate" onclick="applyTheme('slate')">
<span class="theme-dot" style="background:hsl(215,30%,14%); border-color:hsl(215,25%,30%);"></span>
Slate
<span class="theme-check">✓</span>
</button>
<button class="theme-option" data-theme-value="emerald" onclick="applyTheme('emerald')">
<span class="theme-dot" style="background:hsl(152,58%,38%);"></span>
Emerald
<span class="theme-check">✓</span>
</button>
<div style="height:1px; background:var(--color-border); margin:0.375rem 0;"></div>
<div style="padding:0.25rem 0.625rem 0.375rem; font-size:0.625rem; color:var(--color-muted-foreground); text-transform:uppercase; letter-spacing:0.05em;">Фон</div>
<label class="theme-option" style="cursor:pointer;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>
</svg>
Обрати фото
<input type="file" id="bg-file-input" accept="image/*" style="display:none" onchange="handleBgFile(this)">
</label>
<div id="bg-opacity-row" style="display:none; padding:0.25rem 0.625rem 0.375rem; align-items:center; gap:0.5rem;">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--color-muted-foreground); flex-shrink:0;">
<circle cx="12" cy="12" r="10"/><path d="M12 2a10 10 0 0 1 0 20V2z" fill="currentColor" stroke="none"/>
</svg>
<input type="range" id="bg-opacity-slider" min="40" max="98" value="88" style="flex:1; accent-color:var(--color-primary); cursor:pointer;" oninput="setBgOpacity(this.value)">
</div>
<button class="theme-option" id="bg-clear-btn" style="display:none;" onclick="clearBgImage()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6 6 18M6 6l12 12"/>
</svg>
Без фону
</button>
</div>
</div>
<!-- Уведомления -->
<button class="btn btn-ghost btn-icon" style="position:relative;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/>
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>
</svg>
<span style="position:absolute; top:4px; right:4px; width:7px; height:7px; background:var(--color-destructive); border-radius:50%;"></span>
</button>
<!-- Аватар пользователя + дропдаун -->
<div style="position:relative;" id="user-menu-wrapper">
<div onclick="toggleUserMenu()" style="width:2rem; height:2rem; border-radius:50%; cursor:pointer; flex-shrink:0; user-select:none; overflow:hidden;
{% if not request.user.photo %}background:var(--color-accent); color:var(--color-accent-foreground); display:flex; align-items:center; justify-content:center; font-size:0.6875rem; font-weight:700;{% endif %}">
{% if request.user.photo %}
<img src="{{ request.user.photo.url }}" alt="" style="width:100%; height:100%; object-fit:cover; display:block;">
{% else %}
{{ request.user|initials }}
{% endif %}
</div>
<div id="user-dropdown" style="display:none; position:absolute; top:calc(100% + 8px); right:0; min-width:200px; background:var(--color-card); border:1px solid var(--color-border); border-radius:var(--radius); box-shadow:0 8px 24px rgba(0,0,0,0.18); z-index:1000; overflow:hidden;">
<!-- Имя пользователя -->
<div style="padding:0.75rem 1rem 0.625rem; border-bottom:1px solid var(--color-border);">
<div style="font-size:0.8125rem; font-weight:600; color:var(--color-foreground);">{{ request.user.get_full_name|default:request.user.username }}</div>
<div style="font-size:0.6875rem; color:var(--color-muted-foreground); margin-top:0.125rem;">{{ request.user.email }}</div>
</div>
<!-- Пункты меню -->
<div style="padding:0.25rem 0;">
<a href="/employees/{{ request.user.pk }}/?tab=overview" hx-get="/employees/{{ request.user.pk }}/?tab=overview" hx-target="#main-content" hx-push-url="/employees/{{ request.user.pk }}/?tab=overview" hx-indicator="#loader" onclick="closeUserMenu()" style="display:flex; align-items:center; gap:0.625rem; padding:0.5rem 1rem; font-size:0.8125rem; color:var(--color-foreground); text-decoration:none; transition:background 0.15s;" onmouseover="this.style.background='var(--color-muted)'" onmouseout="this.style.background='transparent'">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
{% trans "Профиль" %}
</a>
{% if request.user|can_section:'settings' %}
<a href="/settings/" hx-get="/settings/" hx-target="#main-content" hx-push-url="/settings/" hx-indicator="#loader" onclick="closeUserMenu()" style="display:flex; align-items:center; gap:0.625rem; padding:0.5rem 1rem; font-size:0.8125rem; color:var(--color-foreground); text-decoration:none; transition:background 0.15s;" onmouseover="this.style.background='var(--color-muted)'" onmouseout="this.style.background='transparent'">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
{% trans "Настройки" %}
</a>
{% endif %}
{% if request.user|can_section:'help' %}
<a href="/help/" hx-get="/help/" hx-target="#main-content" hx-push-url="/help/" hx-indicator="#loader" onclick="closeUserMenu()" style="display:flex; align-items:center; gap:0.625rem; padding:0.5rem 1rem; font-size:0.8125rem; color:var(--color-foreground); text-decoration:none; transition:background 0.15s;" onmouseover="this.style.background='var(--color-muted)'" onmouseout="this.style.background='transparent'">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
{% trans "Справка" %}
</a>
{% endif %}
<div style="height:1px; background:var(--color-border); margin:0.25rem 0;"></div>
<form method="post" action="{% url 'logout' %}" style="margin:0;">
{% csrf_token %}
<button type="submit" style="width:100%; display:flex; align-items:center; gap:0.625rem; padding:0.5rem 1rem; font-size:0.8125rem; color:var(--color-destructive); background:none; border:none; cursor:pointer; text-align:left; transition:background 0.15s;" onmouseover="this.style.background='var(--color-muted)'" onmouseout="this.style.background='transparent'">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
{% trans "Выход" %}
</button>
</form>
</div>
</div>
</div>
</div>
</header>
{% if dev_access_preview_active %}
<div style="flex-shrink:0; background:color-mix(in srgb, #f59e0b 18%, var(--color-card)); border-bottom:1px solid color-mix(in srgb, #d97706 40%, var(--color-border)); padding:0.35rem 1.25rem; font-size:0.75rem; color:var(--color-foreground); display:flex; align-items:center; justify-content:space-between; gap:0.75rem;">
<span>{% trans "Режим разработки" %}: {% trans "меню и доступы как" %} <strong>{{ dev_access_preview_label }}</strong></span>
<form method="post" action="{% url 'access_preview' %}" style="margin:0;">
{% csrf_token %}
<input type="hidden" name="preview" value="">
<input type="hidden" name="next" value="">
<button type="submit" class="btn btn-ghost" style="font-size:0.7rem; padding:0.15rem 0.5rem; height:auto;"
onclick="this.form.next.value = window.location.pathname + window.location.search;">
{% trans "Сбросить" %}
</button>
</form>
</div>
{% endif %}
<!-- Контент (HTMX заменяет содержимое) -->
<div style="flex:1; min-height:0; position:relative; display:flex; flex-direction:column;">
<div id="page-preloader" class="page-preloader" aria-hidden="true" aria-live="polite">
<div class="page-preloader-spinner" role="status" aria-label="{% trans 'Загрузка' %}"></div>
</div>
<main id="main-content" style="flex:1; overflow-y:auto; background:var(--color-background); padding:1.5rem;">
{% if page_template %}
{% include page_template %}
{% else %}
<div id="page-loader" style="padding:2rem;color:var(--color-muted-foreground);font-size:0.875rem;">{% trans "Загрузка..." %}</div>
{% endif %}
</main>
</div>
</div>
<!-- Контейнер модалок -->
<div id="modal-container"></div>
<!-- ══════════════════ BOTTOM NAV (mobile) ══════════════════ -->
<nav class="bottom-nav" id="bottom-nav">
<a href="/dashboard/" class="bottom-nav-item"
hx-get="/dashboard/" hx-target="#main-content" hx-push-url="/dashboard/" hx-indicator="#loader"
onclick="bottomNavClick(this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>
</svg>
<span>{% trans "Главная" %}</span>
</a>
{% if request.user|can_section:'tasks' %}
<a href="/tasks/" class="bottom-nav-item"
hx-get="/tasks/" hx-target="#main-content" hx-push-url="/tasks/" hx-indicator="#loader"
onclick="bottomNavClick(this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect width="8" height="4" x="8" y="2" rx="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
<path d="m9 14 2 2 4-4"/>
</svg>
<span>{% trans "Задачи" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'hr_portal' %}
<a href="/hr-portal/" class="bottom-nav-item"
hx-get="/hr-portal/" hx-target="#main-content" hx-push-url="/hr-portal/" hx-indicator="#loader"
onclick="bottomNavClick(this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span>{% trans "Люди" %}</span>
</a>
{% endif %}
{% if request.user|can_section:'chat' %}
<a href="/chat/" class="bottom-nav-item"
hx-get="/chat/" hx-target="#main-content" hx-push-url="/chat/" hx-indicator="#loader"
onclick="bottomNavClick(this)">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/>
</svg>
<span>{% trans "Чат" %}</span>
</a>
{% endif %}
<button class="bottom-nav-item" onclick="toggleSidebar()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</svg>
<span>{% trans "Меню" %}</span>
</button>
</nav>
</div>
<script>
(function () {
var loaderBar = document.getElementById('loader');
var pagePreloader = document.getElementById('page-preloader');
var pending = 0;
function isMainNav(detail) {
if (!detail) return false;
var target = detail.target;
if (target && target.id === 'main-content') return true;
// htmx.ajax / redirects may expose elt with hx-target
var elt = detail.elt;
if (!elt || !elt.getAttribute) return false;
var hxTarget = elt.getAttribute('hx-target');
return hxTarget === '#main-content';
}
function showPageLoader() {
pending += 1;
if (loaderBar) loaderBar.classList.add('is-active');
if (pagePreloader) {
pagePreloader.classList.add('is-active');
pagePreloader.setAttribute('aria-hidden', 'false');
}
}
function hidePageLoader() {
pending = Math.max(0, pending - 1);
if (pending > 0) return;
if (loaderBar) loaderBar.classList.remove('is-active');
if (pagePreloader) {
pagePreloader.classList.remove('is-active');
pagePreloader.setAttribute('aria-hidden', 'true');
}
}
document.body.addEventListener('htmx:beforeRequest', function (e) {
if (isMainNav(e.detail)) showPageLoader();
});
document.body.addEventListener('htmx:afterSwap', function (e) {
if (e.detail && e.detail.target && e.detail.target.id === 'main-content') {
hidePageLoader();
}
});
document.body.addEventListener('htmx:afterRequest', function (e) {
if (!isMainNav(e.detail)) return;
var xhr = e.detail.xhr;
var redirected = xhr && xhr.getResponseHeader && xhr.getResponseHeader('HX-Redirect');
if (e.detail.failed || redirected) hidePageLoader();
});
document.body.addEventListener('htmx:sendError', function (e) {
if (isMainNav(e.detail)) hidePageLoader();
});
window.__showPageLoader = showPageLoader;
window.__hidePageLoader = hidePageLoader;
})();
// Активный пункт меню по текущему URL
function setActiveNavItem() {
const currentPath = window.location.pathname;
document.querySelectorAll('.nav-item').forEach(item => {
const hxGet = item.getAttribute('hx-get');
item.classList.toggle('active', !!hxGet && hxGet === currentPath);
});
syncBottomNav(currentPath);
}
document.addEventListener('DOMContentLoaded', function() {
const mainContent = document.getElementById('main-content');
// Плейсхолдер определяем по элементу, а не по тексту (текст переводится)
const isPlaceholder = !!mainContent.querySelector('#page-loader');
// Если сервер не отдал страницу — грузим по URL
if (isPlaceholder) {
const currentPath = window.location.pathname;
const pageUrl = currentPath === '/' ? '/dashboard/' : currentPath;
htmx.ajax('GET', pageUrl, {target:'#main-content', swap:'innerHTML'});
}
setActiveNavItem();
(function initPortalSearch() {
var wrap = document.getElementById('header-search');
var input = document.getElementById('portal-search-input');
var panel = document.getElementById('search-results');
if (!wrap || !input || !panel) return;
function openSearch() {
panel.hidden = false;
input.setAttribute('aria-expanded', 'true');
}
function closeSearch() {
panel.hidden = true;
input.setAttribute('aria-expanded', 'false');
}
window.closePortalSearch = closeSearch;
input.addEventListener('focus', openSearch);
document.addEventListener('click', function (e) {
if (!wrap.contains(e.target)) closeSearch();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeSearch();
});
document.body.addEventListener('htmx:afterSwap', function (e) {
if (e.detail && e.detail.target && e.detail.target.id === 'search-results') {
openSearch();
}
});
})();
// Восстановление темы и фона
var savedTheme = localStorage.getItem('theme') || 'light';
document.querySelectorAll('.theme-option').forEach(function(btn) {
btn.classList.toggle('active', btn.getAttribute('data-theme-value') === savedTheme);
});
var savedBg = localStorage.getItem('bg-image');
if (savedBg) {
var savedOpacity = parseFloat(localStorage.getItem('bg-opacity') || '0.88');
applyBgImage(savedBg, savedOpacity);
}
});
// После загрузки фрагмента — активный пункт и init страницы
document.body.addEventListener('htmx:afterSwap', function(e) {
if (e.detail.target.id !== 'main-content') return;
setActiveNavItem();
if (typeof pharmaInit === 'function') pharmaInit();
if (typeof empInit === 'function') empInit();
if (typeof ttInit === 'function') ttInit();
if (typeof hrPortalInit === 'function') hrPortalInit();
if (typeof empProfileInit === 'function') empProfileInit();
// Чат: inline <script>chatInit()</script> в HTMX-swap часто не выполняется —
// без этого списка каналов зависает на «Загрузка…».
if (typeof chatInit === 'function' && document.getElementById('chat-config-data')) {
chatInit();
}
});
</script>
</body>
</html>