bfd67241c4
- ad_connector: restrict provisioning to office employees (user_location_type='office') in sync service, signals and command; disable AD account on transfer out of office, re-enable on return - ad_connector: /ad-mapping/ page (staff only) — match office employees to AD accounts with suggestions by email/login/full name, link/unlink/ create actions, list of unmatched domain accounts; sidebar nav item - core: auto-rebuild treenode Directory tree (_path/_depth) after import-export imports and post_migrate; "rebuild tree" button in DirectoryAdmin and rebuild_directory_tree management command Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
342 lines
18 KiB
HTML
342 lines
18 KiB
HTML
{% load static %}
|
|
<!DOCTYPE html>
|
|
<html lang="uk">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>АКС HR — Портал управління</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 'assets/js/pharmacies.js' %}"></script>
|
|
<script src="{% static 'assets/js/employees.js' %}"></script>
|
|
<script src="{% static 'assets/js/structure.js' %}"></script>
|
|
<script src="{% static 'assets/js/timetrack.js' %}"></script>
|
|
</head>
|
|
<body>
|
|
|
|
<script>
|
|
function toggleSidebar() {
|
|
const s = document.getElementById('sidebar');
|
|
s.classList.toggle('collapsed');
|
|
}
|
|
function setActive(el) {
|
|
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
|
|
el.classList.add('active');
|
|
}
|
|
</script>
|
|
|
|
<!-- Loader bar (HTMX) -->
|
|
<div class="loader-bar htmx-indicator" id="loader"></div>
|
|
|
|
<div style="display:flex; height:100vh; overflow:hidden;">
|
|
|
|
<!-- ══════════════════ SIDEBAR ══════════════════ -->
|
|
<aside id="sidebar" class="sidebar">
|
|
|
|
<!-- Шапка -->
|
|
<div class="sidebar-header">
|
|
<div class="sidebar-logo">HR</div>
|
|
<div class="sidebar-logo-text">
|
|
<div style="color:var(--color-sidebar-accent-foreground); font-size:0.875rem; font-weight:500; line-height:1.2;">АКС HR</div>
|
|
<div style="color:var(--color-sidebar-foreground); font-size:0.6875rem; opacity:0.6; line-height:1.2;">Портал управління</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">Основне</div>
|
|
<div style="display:flex; flex-direction:column; gap:2px;">
|
|
|
|
<a href="#" class="nav-item active"
|
|
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">
|
|
<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/>
|
|
<rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/>
|
|
</svg>
|
|
<span class="nav-label-text">Дашборд</span>
|
|
</a>
|
|
|
|
<a href="#" class="nav-item"
|
|
hx-get="/employees/"
|
|
hx-target="#main-content"
|
|
hx-push-url="/employees/"
|
|
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">Співробітники</span>
|
|
</a>
|
|
|
|
<a href="#" 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">Структура</span>
|
|
</a>
|
|
|
|
<a href="#" 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">Аптеки</span>
|
|
<span class="nav-badge nav-badge-amber">14</span>
|
|
</a>
|
|
|
|
<a href="#" class="nav-item"
|
|
hx-get="/recruiting/"
|
|
hx-target="#main-content"
|
|
hx-push-url="/recruiting/"
|
|
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"/>
|
|
<line x1="19" x2="19" y1="8" y2="14"/><line x1="22" x2="16" y1="11" y2="11"/>
|
|
</svg>
|
|
<span class="nav-label-text">Рекрутинг</span>
|
|
<span class="nav-badge nav-badge-red">27</span>
|
|
</a>
|
|
|
|
<a href="#" class="nav-item"
|
|
hx-get="/timetrack/"
|
|
hx-target="#main-content"
|
|
hx-push-url="/timetrack/"
|
|
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"/><polyline points="12 6 12 12 16 14"/>
|
|
</svg>
|
|
<span class="nav-label-text">Облік робочого часу</span>
|
|
</a>
|
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Инструменты -->
|
|
<div>
|
|
<div class="nav-section-label">Інструменти</div>
|
|
<div style="display:flex; flex-direction:column; gap:2px;">
|
|
|
|
<a href="#" 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">Календар</span>
|
|
</a>
|
|
|
|
<a href="#" 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">Звіти</span>
|
|
</a>
|
|
|
|
<a href="#" 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">Документи</span>
|
|
</a>
|
|
|
|
{% if request.user.is_staff %}
|
|
<a href="#" 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">Active Directory</span>
|
|
</a>
|
|
{% endif %}
|
|
|
|
<a href="#" 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">Налаштування</span>
|
|
</a>
|
|
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Футер сайдбара -->
|
|
<div class="sidebar-footer">
|
|
<div style="width:1.75rem; height:1.75rem; border-radius:50%; background:var(--color-sidebar-primary); display:flex; align-items:center; justify-content:center; color:var(--color-sidebar-primary-foreground); font-size:0.6875rem; font-weight:600; flex-shrink:0;">
|
|
АК
|
|
</div>
|
|
<div class="sidebar-logo-text" style="min-width:0; flex:1;">
|
|
<div style="color:var(--color-sidebar-accent-foreground); font-size:0.75rem; font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ request.user.username }}</div>
|
|
<div style="color:var(--color-sidebar-foreground); font-size:0.6rem; opacity:0.6; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ request.user.email }}</div>
|
|
</div>
|
|
<div class="footer-actions">
|
|
<a href="{% url 'logout' %}" class="btn btn-ghost btn-icon" title="Вийти" style="color:var(--color-sidebar-foreground); opacity:0.8; display:flex; align-items:center; justify-content:center;">
|
|
<svg width="14" height="14" 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" x2="9" y1="12" y2="12"/>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
</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 style="display:flex; align-items:center; gap:0.5rem; background:var(--color-muted); border:1px solid var(--color-border); border-radius:var(--radius); padding:0.4375rem 0.75rem; width:18rem;">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--color-muted-foreground); flex-shrink:0;">
|
|
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
|
</svg>
|
|
<input type="text"
|
|
placeholder="Пошук співробітників, аптек..."
|
|
class="input"
|
|
style="border:none; background:transparent; padding:0; box-shadow:none;"
|
|
hx-get="/api/search"
|
|
hx-trigger="keyup changed delay:300ms"
|
|
hx-target="#search-results"
|
|
name="q">
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:flex; align-items:center; gap:0.5rem;">
|
|
<!-- Уведомления -->
|
|
<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>
|
|
|
|
<!-- Кнопка добавить -->
|
|
<button class="btn btn-primary"
|
|
hx-get="/modal-add-employee/"
|
|
hx-target="#modal-container"
|
|
hx-trigger="click">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="12" x2="12" y1="5" y2="19"/><line x1="5" x2="19" y1="12" y2="12"/>
|
|
</svg>
|
|
Додати
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Контент (HTMX заменяет содержимое) -->
|
|
<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;">Завантаження...</div>
|
|
{% endif %}
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Контейнер модалок -->
|
|
<div id="modal-container"></div>
|
|
</div>
|
|
|
|
<script>
|
|
// Устанавливаем активный пункт меню на основе текущего URL
|
|
function setActiveNavItem() {
|
|
const currentPath = window.location.pathname;
|
|
document.querySelectorAll('.nav-item').forEach(item => {
|
|
const hxGet = item.getAttribute('hx-get');
|
|
if (hxGet && hxGet === currentPath) {
|
|
item.classList.add('active');
|
|
} else {
|
|
item.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Загружаем нужную страницу при старте
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const mainContent = document.getElementById('main-content');
|
|
const isPlaceholder = mainContent.textContent.trim() === 'Завантаження...';
|
|
|
|
// Если контент пустой или это плейсхолдер, загружаем нужную страницу на основе URL
|
|
if (isPlaceholder) {
|
|
const currentPath = window.location.pathname;
|
|
const pageUrl = currentPath === '/' ? '/dashboard/' : currentPath;
|
|
htmx.ajax('GET', pageUrl, {target:'#main-content', swap:'innerHTML'});
|
|
}
|
|
|
|
// Всегда устанавливаем активный пункт меню после загрузки DOM
|
|
setActiveNavItem();
|
|
});
|
|
|
|
// После загрузки фрагмента — вызываем 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();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|