Corp/backend/static/scss/components/_calendar-events.scss
Игорь Надеин 28e2f1615f feat: реализован drag & drop для перемещения событий между календарями
- Создан модуль calendarEventDragDrop.js с классами DragDropState и CalendarUtils
- Реализовано перетаскивание событий между календарями через модальное окно
- Добавлена кнопка 'Переместить все' для массового перемещения
- Модальное окно отображает источник и цель перемещения
- Поддержка legacy-календарей (company/department/personal)
- Добавлены стили для подсветки drop-target (папки и календари)
- Backend: исправлена обработка null значений в serializer
- Backend: добавлен endpoint list_raw для получения немодерируемых событий
- Использованы стандартные Bootstrap стили для модалов
- Исправлено всплытие событий dragover/drop (stopPropagation)
- Эмодзи иконки для типов календарей (����)
- Toast уведомления об успехе/ошибках перемещения
2026-02-12 15:59:53 +03:00

536 lines
16 KiB
SCSS
Raw Permalink 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.

/**
* _calendar-events.scss
* Современное компактное отображение событий FullCalendar
* Двухцветная схема: граница календаря + фон события
*/
// ============================================================================
// ПЕРЕМЕННЫЕ
// ============================================================================
$event-border-width: 3px;
$event-padding-y: 1px; // Уменьшено с 2px до 1px
$event-padding-x: 4px;
$event-margin: 1px;
$event-radius: 3px;
$event-font-size: 10px;
$event-line-height: 1.1; // Уменьшено с 1.2 до 1.1
$event-max-lines: 2;
// ============================================================================
// БАЗОВЫЕ СТИЛИ СОБЫТИЙ
// ============================================================================
.fc-event {
// Сброс базовых стилей
border: none !important;
border-radius: $event-radius !important;
margin: $event-margin 2px !important;
padding: $event-padding-y $event-padding-x !important;
cursor: pointer;
overflow: hidden;
// Компактность
font-size: $event-font-size;
line-height: $event-line-height;
// Плавные переходы
transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease, z-index 0s;
// Позиционирование для z-index
position: relative;
z-index: 3; // Базовый z-index для всех событий
// Минимальная высота - практически равна размеру текста
min-height: 14px; // Уменьшено с 18px (10px шрифт + 1px отступы сверху/снизу + запас)
// Убираем внутренние отступы FC
.fc-event-main {
padding: 0;
}
}
// ============================================================================
// ДВУХЦВЕТНАЯ СХЕМА (КАЛЕНДАРЬ + СОБЫТИЕ)
// ============================================================================
// События С цветом календаря (новая система)
.fc-event[data-has-calendar-color="true"] {
// Левая граница = цвет календаря (устанавливается через JS с шириной и цветом)
// border-left устанавливается через JS: border-left: 4px solid <calendarColor>
padding-left: ($event-padding-x + 1px) !important;
// Фон = цвет события с прозрачностью (устанавливается через JS)
// Прозрачность позволяет видеть подложку
// Лёгкая тень для объёма
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
}
// События БЕЗ цвета календаря (legacy система)
.fc-event:not([data-has-calendar-color="true"]) {
// Левая граница = цвет события (устанавливается через JS)
// border-left устанавливается через JS: border-left: 3px solid <eventColor>
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06);
}
// ============================================================================
// ТЕКСТ СОБЫТИЯ
// ============================================================================
.fc-event-title {
font-weight: 500;
color: #ffffff;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
// Ограничение строк
display: -webkit-box;
-webkit-line-clamp: $event-max-lines;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
.fc-event-time {
font-weight: 400;
font-size: ($event-font-size - 1px);
opacity: 0.9;
margin-right: 2px;
}
// ============================================================================
// HOVER И INTERACTIVE СОСТОЯНИЯ
// ============================================================================
.fc-event:hover {
transform: translateY(-1px);
z-index: 200 !important; // Очень высокий z-index при hover, чтобы быть поверх всего
&[data-has-calendar-color="true"] {
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
&:not([data-has-calendar-color="true"]) {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
.fc-event:active {
transform: translateY(0);
z-index: 200 !important;
}
// ============================================================================
// ТИПЫ СОБЫТИЙ
// ============================================================================
// События на весь день
.fc-event.fc-daygrid-block-event {
padding: ($event-padding-y + 1px) $event-padding-x !important;
min-height: 20px;
}
// ============================================================================
// МНОГОДНЕВНЫЕ СОБЫТИЯ (УЛУЧШЕННАЯ ВИЗУАЛИЗАЦИЯ)
// ============================================================================
// Базовые стили для всех многодневных событий
.fc-event.fc-daygrid-event {
// Убираем отступы между сегментами
margin-top: 0 !important;
margin-bottom: 2px !important;
// Компактная высота для многодневных событий
min-height: 14px; // Уменьшено с 22px - теперь равна обычным событиям
// Выравнивание контента
display: flex;
align-items: center;
// Растягиваем на всю ширину ячейки
width: 100% !important;
}
// Событие начинается (первый день)
.fc-event.fc-event-start:not(.fc-event-end) {
border-top-left-radius: $event-radius !important;
border-bottom-left-radius: $event-radius !important;
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
margin-right: 0 !important;
padding-right: ($event-padding-x + 2px) !important;
// Визуальный индикатор продолжения
position: relative;
&::after {
content: '›';
position: absolute;
right: 2px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
font-weight: bold;
opacity: 0.7;
color: rgba(255, 255, 255, 0.8);
}
}
// Событие продолжается (средние дни)
.fc-event:not(.fc-event-start):not(.fc-event-end) {
border-radius: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
padding-left: ($event-padding-x + 2px) !important;
padding-right: ($event-padding-x + 2px) !important;
// Визуальные индикаторы продолжения с обеих сторон
position: relative;
&::before {
content: '‹';
position: absolute;
left: 2px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
font-weight: bold;
opacity: 0.7;
color: rgba(255, 255, 255, 0.8);
}
&::after {
content: '›';
position: absolute;
right: 2px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
font-weight: bold;
opacity: 0.7;
color: rgba(255, 255, 255, 0.8);
}
}
// Событие заканчивается (последний день)
.fc-event:not(.fc-event-start).fc-event-end {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
border-top-right-radius: $event-radius !important;
border-bottom-right-radius: $event-radius !important;
margin-left: 0 !important;
padding-left: ($event-padding-x + 2px) !important;
// Визуальный индикатор начала
position: relative;
&::before {
content: '‹';
position: absolute;
left: 2px;
top: 50%;
transform: translateY(-50%);
font-size: 14px;
font-weight: bold;
opacity: 0.7;
color: rgba(255, 255, 255, 0.8);
}
}
// Событие в один день (начинается и заканчивается)
.fc-event.fc-event-start.fc-event-end {
border-radius: $event-radius !important;
margin-left: 2px !important;
margin-right: 2px !important;
// Убираем индикаторы
&::before,
&::after {
display: none;
}
}
// Для событий с цветом календаря - индикаторы белые
.fc-event[data-has-calendar-color="true"] {
&::before,
&::after {
color: #ffffff;
}
}
// ============================================================================
// СПЕЦИАЛЬНЫЕ МЕТКИ
// ============================================================================
// Ежегодные события (паттерн)
.fc-event[data-recurrence="annual"] {
background-image: repeating-linear-gradient(
45deg,
transparent,
transparent 8px,
rgba(255, 255, 255, 0.08) 8px,
rgba(255, 255, 255, 0.08) 16px
) !important;
}
// ============================================================================
// УПРАВЛЕНИЕ НАКЛАДЫВАЮЩИМИСЯ СОБЫТИЯМИ
// ============================================================================
// УДАЛЕНО: overflow: visible на таблице - это ломает позиционирование FullCalendar
// Вместо этого просто убираем max-height ограничение и позволяем FullCalendar
// самостоятельно управлять размещением событий
// Ячейка дня
.fc-daygrid-day-frame {
min-height: 80px;
position: relative;
// Убираем границу, чтобы не мешать многодневным событиям
// border: 1px solid var(--fc-border-color);
}
// События внутри дня
.fc-daygrid-day-events {
// ВАЖНО: НЕ ограничиваем высоту - пусть FullCalendar сам управляет размещением
// max-height: 72px; // ОТКЛЮЧЕНО
margin-bottom: 0;
padding-bottom: 0;
position: relative;
}
// Контейнер событий
.fc-daygrid-event-harness {
margin-bottom: 1px;
position: relative;
z-index: 2; // Базовый z-index для контейнера обычных событий
}
// Контейнер для многодневных событий
.fc-daygrid-event-harness-abs {
// КРИТИЧНО: position: absolute нужен для работы inline top/left/right от FullCalendar
position: absolute;
left: 0;
right: 0;
// FullCalendar автоматически рассчитывает top для каждого ряда
// Главное: dayMaxEventRows в JS должно быть числом (не true/false)
// Z-index ниже, чем у обычных событий, чтобы однодневные были поверх многодневных
z-index: 1;
}
// Убираем лишние отступы для многодневных событий
.fc-daygrid-day-bg + .fc-daygrid-day-events {
margin-top: 2px;
}
// ============================================================================
// ССЫЛКА "+N ЕЩЁ"
// ============================================================================
.fc-daygrid-more-link {
color: var(--bs-primary) !important;
font-weight: 600 !important;
font-size: 10px !important;
padding: 2px 4px !important;
margin: 1px 2px !important;
border-radius: 2px !important;
background: color-mix(in srgb, var(--bs-primary), transparent 90%) !important;
transition: background-color 0.15s ease;
display: inline-block;
&:hover {
background: color-mix(in srgb, var(--bs-primary), transparent 85%) !important;
text-decoration: none !important;
}
}
// ============================================================================
// POPOVER ДЛЯ МНОЖЕСТВА СОБЫТИЙ
// ============================================================================
.fc-popover {
border-radius: 8px !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15) !important;
border: 1px solid var(--bs-border-color) !important;
max-width: 280px;
z-index: 1050 !important;
}
.fc-popover-header {
background: linear-gradient(135deg, var(--bs-primary) 0%, color-mix(in srgb, var(--bs-primary), #000 15%) 100%) !important;
color: white !important;
padding: 6px 10px !important;
border-radius: 8px 8px 0 0 !important;
font-weight: 600;
font-size: 11px;
}
.fc-popover-body {
padding: 6px !important;
max-height: 300px;
overflow-y: auto;
// Компактные события в popover
.fc-event {
margin-bottom: 2px !important;
}
}
// Кастомный scrollbar для popover
.fc-popover-body::-webkit-scrollbar {
width: 4px;
}
.fc-popover-body::-webkit-scrollbar-track {
background: transparent;
}
.fc-popover-body::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--bs-body-color), transparent 75%);
border-radius: 2px;
&:hover {
background: color-mix(in srgb, var(--bs-body-color), transparent 60%);
}
}
// ============================================================================
// ДНИ КАЛЕНДАРЯ
// ============================================================================
// Текущий день
.fc .fc-day-today {
background-color: color-mix(in srgb, var(--bs-primary), transparent 95%) !important;
.fc-daygrid-day-number {
background-color: var(--bs-primary) !important;
color: white !important;
border-radius: 50%;
min-width: 24px;
min-height: 24px;
width: auto;
height: auto;
line-height: 1;
display: inline-flex !important;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 12px;
padding: 4px !important;
border: none !important; // Убираем рамку из rightbar-calendar
}
}
// Выходные дни
.fc-day-sat,
.fc-day-sun {
background-color: color-mix(in srgb, var(--bs-danger), transparent 98%);
}
// Прошедшие дни
.fc-day-past:not(.fc-day-today) {
background-color: color-mix(in srgb, var(--bs-body-color), transparent 98%);
.fc-daygrid-day-number {
color: color-mix(in srgb, var(--bs-body-color), transparent 40%);
}
.fc-event {
opacity: 0.7;
}
}
// Номер дня
.fc-daygrid-day-number {
padding: 3px 5px;
font-weight: 500;
font-size: 11px;
color: var(--bs-body-color);
}
// ============================================================================
// ВЫДЕЛЕНИЕ ВЫБРАННОЙ ДАТЫ
// ============================================================================
.fc-highlight {
background: color-mix(in srgb, var(--bs-primary), transparent 90%) !important;
border-radius: 4px;
}
// ============================================================================
// АНИМАЦИИ
// ============================================================================
@keyframes fadeInEvent {
from {
opacity: 0;
transform: translateY(-3px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fc-event {
animation: fadeInEvent 0.2s ease;
}
// ============================================================================
// АДАПТИВНОСТЬ
// ============================================================================
@media (max-width: 768px) {
.fc-daygrid-day-frame {
min-height: 60px;
}
.fc-daygrid-day-events {
max-height: 54px; // ~3 события
}
.fc-event {
font-size: 9px;
padding: 1px 3px !important;
min-height: 16px;
}
.fc-event-title {
-webkit-line-clamp: 1; // Только 1 строка на мобильных
}
}
// ============================================================================
// ЗАГОЛОВКИ КОЛОНОК
// ============================================================================
.fc-col-header-cell {
font-weight: 600;
font-size: 11px;
background: var(--bs-tertiary-bg);
border-color: var(--bs-border-color) !important;
padding: 6px 4px;
}
// ============================================================================
// УЛУЧШЕНИЯ ПРОИЗВОДИТЕЛЬНОСТИ
// ============================================================================
.fc-event {
// Включаем аппаратное ускорение для трансформаций
will-change: transform;
// Принудительное использование GPU слоя при hover
&:hover {
will-change: transform, box-shadow;
}
}
// Отключаем will-change после завершения анимации
.fc-event:not(:hover) {
will-change: auto;
}