Corp/backend/templates/base.html

244 lines
10 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 static api_tags %}
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="color-scheme" content="light dark">
{% if user.is_authenticated %}
<meta name="api-access" content="{% api_access_token request %}">
<meta name="user-id" content="{{ user.id }}">
{% endif %}
{# КРИТИЧНО: Применяем тему ДО загрузки CSS для предотвращения FOUC #}
<script src="{% static 'js/utils/themeInitializer.js' %}"></script>
<title>{% block title %}HiRo{% endblock %}</title>
<link rel="icon" type="image/png" sizes="32x32" href="{% static 'img/favicon-32.png' %}">
<link rel="apple-touch-icon" href="{% static 'img/apple-touch-icon.png' %}">
<link rel="icon" type="image/png" sizes="192x192" href="{% static 'img/logo-192.png' %}">
{# Preload критичных CSS для ускорения загрузки #}
<link rel="preload" href="{% static 'css/bootstrap-custom.css' %}" as="style">
<link rel="preload" href="{% static 'css/variables.css' %}" as="style">
<link rel="preload" href="{% static 'css/components/common.css' %}" as="style">
<!-- Bootstrap / Icons / FullCalendar -->
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"> -->
<!-- Кастомный Bootstrap с нашей системой отступов -->
<link rel="stylesheet" href="{% static 'css/bootstrap-custom.css' %}">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<!-- CSS Components -->
<link rel="stylesheet" href="{% static 'css/variables.css' %}">
<link rel="stylesheet" href="{% static 'css/components/common.css' %}">
<link rel="stylesheet" href="{% static 'css/components/spacing-utils.css' %}">
<link rel="stylesheet" href="{% static 'css/components/layout-spacing.css' %}">
<link rel="stylesheet" href="{% static 'css/components/buttons.css' %}">
<link rel="stylesheet" href="{% static 'css/components/ios-search.css' %}">
<link rel="stylesheet" href="{% static 'css/components/base-app.css' %}">
<link rel="stylesheet" href="{% static 'css/components/ios-components.css' %}">
<link rel="stylesheet" href="{% static 'css/components/card-list.css' %}">
<link rel="stylesheet" href="{% static 'css/notifications/notifications.css' %}">
{% block extra_css %}{% endblock %}
</head>
<body class="min-vh-100">
{% include "includes/navbar.html" %}
<div class="container-xxl">
<div class="app-layout {% if user.is_authenticated %}with-sidebar with-rightbar{% endif %}">
{% if user.is_authenticated %}
{% include "includes/sidebar.html" %}
{% endif %}
<main class="main">
{% block content %}{% endblock %}
</main>
{% if user.is_authenticated %}
{% include "includes/rightbar_calendar.html" %}
{% endif %}
</div>
</div>
<!-- JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous" defer></script>
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.8/index.global.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.8/locales-all.global.min.js" defer></script>
{# Универсальный модуль для автоматического расширения textarea #}
<script src="{% static 'js/components/textareaAutoExpand.js' %}" defer></script>
{# Импорт утилит из модулей #}
<script type="module">
import { esc, norm, escAttr, truncate, getInitials } from '{% static "js/utils/stringUtils.js" %}';
import { debounce, throttle, delay } from '{% static "js/utils/timing.js" %}';
import { smoothScrollTo, scrollToTop, isElementInViewport } from '{% static "js/utils/scroll.js" %}';
import { initNavbarHeight } from '{% static "js/components/navbarHeight.js" %}';
import { initLikeHandler } from '{% static "js/components/likeHandler.js" %}';
// Экспортируем в window для обратной совместимости с существующим кодом
window.esc = esc;
window.norm = norm;
window.escAttr = escAttr;
window.truncate = truncate;
window.getInitials = getInitials;
window.debounce = debounce;
window.throttle = throttle;
window.delay = delay;
window.smoothScrollTo = smoothScrollTo;
window.scrollToTop = scrollToTop;
window.isElementInViewport = isElementInViewport;
// Инициализация компонентов
document.addEventListener('DOMContentLoaded', () => {
const navbarHeight = initNavbarHeight();
const likeHandler = initLikeHandler();
if (navbarHeight) window.navbarHeight = navbarHeight;
if (likeHandler) window.likeHandler = likeHandler;
});
</script>
{# ========== CHAT MODULES ========== #}
{% if user.is_authenticated %}
<script type="module">
import { initChatAvatarMap } from "{% static 'js/components/chatAvatarMap.js' %}";
import { initChatMarkRead } from "{% static 'js/components/chatMarkRead.js' %}";
import { initChatWebSocket } from "{% static 'js/components/chatWebSocket.js' %}";
import { initChatListFilter } from "{% static 'js/components/chatListFilter.js' %}";
import { initChatBadgeManager } from "{% static 'js/components/chatBadgeManager.js' %}";
import { initChatListRealtime } from "{% static 'js/components/chatListRealtime.js' %}";
// Инициализация карты аватаров
const avatarMap = initChatAvatarMap({
"{{ user.id }}": {% if user.avatar %}"{{ user.avatar.url }}"{% else %}""{% endif %}
});
// Инициализация badge менеджера (слушает события chat:read)
const badgeManager = initChatBadgeManager();
// Инициализация фильтрации списка чатов
const chatListFilter = initChatListFilter();
// Инициализация realtime-обновлений списка чатов
const chatListRealtime = initChatListRealtime({
meId: {{ user.id|default:"0" }},
badgeManager: badgeManager
});
// Инициализация отметки прочитанного (если открыт конкретный чат)
{% if chat %}
const markReadApi = initChatMarkRead({
chatId: {{ chat.id }},
meId: {{ user.id|default:"0" }},
scrollContainerId: 'chatScroll',
textareaId: 'id_content',
formId: 'chatForm',
scrollBtnId: 'scrollBtn',
markReadUrl: "{% url 'communications:chat_mark_read' chat.pk %}",
initialLastReadTs: {{ chat.last_read_timestamp|default:"null" }}
});
// Инициализация WebSocket для чата
const chatWebSocket = initChatWebSocket({
chatId: {{ chat.id }},
meId: {{ user.id|default:"0" }},
scrollContainerId: 'chatScroll',
formId: 'chatForm',
textareaId: 'id_content',
typingIndicatorId: 'typing',
profileUrl: "{% url 'employees:profile' %}",
detailUrlTemplate: "{% url 'employees:employee_detail' 0 %}",
avatarMap: avatarMap.getAll(),
markReadApi: markReadApi,
bindFormSubmit: false
});
if (chatWebSocket) {
window.chatWebSocketApi = chatWebSocket;
window.dispatchEvent(new CustomEvent('chat:ws-ready', { detail: chatWebSocket }));
}
{% endif %}
// Глобальный обновлятель бейджа в сайдбаре
(function initSidebarBadge() {
const meId = {{ user.id|default:"0" }};
const badge = document.getElementById('sidebarChatBadge');
function setBadge(n) {
if (!badge) return;
const v = Math.max(0, Number(n) || 0);
badge.dataset.count = String(v);
badge.textContent = v > 99 ? '99+' : String(v);
badge.classList.toggle('d-none', v <= 0);
}
// Стартовое значение
setBadge(badge?.dataset.count || 0);
// Карта непрочитанных по чатам
const chatUnread = Object.create(null);
document.querySelectorAll('.chat-row[data-chat-id]').forEach(row => {
const id = String(row.getAttribute('data-chat-id'));
const cnt = Number(row.querySelector('[data-unread-count]')?.textContent || 0);
chatUnread[id] = cnt;
});
function recompute() {
let total = 0;
for (const k in chatUnread) {
total += Number(chatUnread[k] || 0);
}
setBadge(total);
}
if (Object.keys(chatUnread).length) recompute();
// Глобальный WebSocket для обновления бейджа
try {
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const ws = new WebSocket(`${proto}://${location.host}/ws/chats/`);
ws.addEventListener('message', (e) => {
try {
const data = JSON.parse(e.data);
if (data.type !== 'list_update') return;
const chatId = String(data.chat_id);
const msg = data.message || {};
const isMine = Number(msg.author_id) === Number(meId);
chatUnread[chatId] = isMine ? 0 : (Number(chatUnread[chatId] || 0) + 1);
recompute();
} catch (_) {}
});
} catch (_) {}
// Обработка события chat:read
window.addEventListener('chat:read', (e) => {
const chatId = String(e?.detail?.chatId || '');
if (!chatId) return;
chatUnread[chatId] = 0;
recompute();
});
})();
</script>
<!-- Notification Manager -->
<script type="module" src="{% static 'js/notifications/notification-manager.js' %}"></script>
{% endif %}
{% block extra_js %}{% endblock %}
</body>
</html>