- Создан модуль calendarEventDragDrop.js с классами DragDropState и CalendarUtils - Реализовано перетаскивание событий между календарями через модальное окно - Добавлена кнопка 'Переместить все' для массового перемещения - Модальное окно отображает источник и цель перемещения - Поддержка legacy-календарей (company/department/personal) - Добавлены стили для подсветки drop-target (папки и календари) - Backend: исправлена обработка null значений в serializer - Backend: добавлен endpoint list_raw для получения немодерируемых событий - Использованы стандартные Bootstrap стили для модалов - Исправлено всплытие событий dragover/drop (stopPropagation) - Эмодзи иконки для типов календарей (����) - Toast уведомления об успехе/ошибках перемещения
172 lines
7.3 KiB
HTML
172 lines
7.3 KiB
HTML
{% 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/app.css' %}" as="style">
|
||
|
||
<!-- Bootstrap / Icons -->
|
||
<!-- Кастомный Bootstrap с нашей системой отступов + все CSS переменные -->
|
||
<link rel="stylesheet" href="{% static 'css/bootstrap-custom.css' %}">
|
||
|
||
<link href="{% static 'vendor/bootstrap-icons/bootstrap-icons.min.css' %}" rel="stylesheet">
|
||
|
||
<!-- Главные стили приложения (скомпилированные из SCSS) -->
|
||
<link rel="stylesheet" href="{% static 'css/app.css' %}">
|
||
|
||
<!-- Message highlight animation -->
|
||
<link rel="stylesheet" href="{% static 'css/components/message-highlight.css' %}">
|
||
|
||
{% block extra_css %}{% endblock %}
|
||
</head>
|
||
<body class="min-vh-100">
|
||
{# Устанавливаем глобальные переменные в самом начале #}
|
||
{% if user.is_authenticated %}
|
||
<script>
|
||
window.currentUserId = {{ user.id }};
|
||
</script>
|
||
{% endif %}
|
||
|
||
{% 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 -->
|
||
<!-- Bootstrap JS (локальная версия) -->
|
||
<script src="{% static 'vendor/bootstrap/bootstrap.bundle.min.js' %}"></script>
|
||
|
||
<!-- FullCalendar (локальные версии) -->
|
||
<script src="{% static 'vendor/fullcalendar/fullcalendar.min.js' %}" defer></script>
|
||
<script src="{% static 'vendor/fullcalendar/locales-all.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>
|
||
|
||
{# ========== UNIVERSAL USER WEBSOCKET ========== #}
|
||
{# Универсальное WebSocket-подключение для всех real-time обновлений #}
|
||
{% if user.is_authenticated %}
|
||
<script type="module">
|
||
import { initUserWebSocket } from "{% static 'js/components/userWebSocket.js' %}";
|
||
|
||
// Базовая инициализация WebSocket (без chat-специфичной логики)
|
||
const userWs = initUserWebSocket({
|
||
userId: {{ user.id }}
|
||
});
|
||
|
||
// Экспортируем в window для доступа из других модулей
|
||
if (userWs) {
|
||
window.userWebSocket = userWs;
|
||
window.dispatchEvent(new CustomEvent('user:ws-ready', {
|
||
detail: { api: userWs, ws: userWs.ws }
|
||
}));
|
||
}
|
||
</script>
|
||
|
||
<!-- Notification Manager -->
|
||
<script type="module" src="{% static 'js/notifications/notification-manager.js' %}"></script>
|
||
|
||
<!-- Web Push Notifications -->
|
||
<script type="module">
|
||
import { pushNotifications } from "{% static 'js/notifications/push-notifications.js' %}";
|
||
|
||
// Инициализация Web Push после загрузки страницы
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
const initialized = await pushNotifications.init();
|
||
|
||
if (initialized) {
|
||
console.log('[WebPush] Инициализирован, подписка:', pushNotifications.isSubscribed);
|
||
|
||
// Если разрешение еще не спрошено - запрашиваем его
|
||
if (pushNotifications.getPermissionStatus() === 'default' && !pushNotifications.isSubscribed) {
|
||
console.log('[WebPush] Запрашиваем разрешение на уведомления...');
|
||
|
||
// Небольшая задержка, чтобы пользователь успел сориентироваться
|
||
setTimeout(async () => {
|
||
const permission = await pushNotifications.requestPermission();
|
||
if (permission === 'granted') {
|
||
console.log('[WebPush] Разрешение получено, подписываемся...');
|
||
await pushNotifications.subscribe();
|
||
} else {
|
||
console.log('[WebPush] Разрешение не получено');
|
||
}
|
||
}, 2000);
|
||
}
|
||
|
||
// Если разрешение дано, но нет подписки - подписываем автоматически
|
||
if (pushNotifications.getPermissionStatus() === 'granted' && !pushNotifications.isSubscribed) {
|
||
console.log('[WebPush] Разрешение есть, подписываемся...');
|
||
await pushNotifications.subscribe();
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
{% endif %}
|
||
|
||
{% block extra_js %}{% endblock %}
|
||
</body>
|
||
</html>
|