{}',
- obj.color, obj.color
+ obj.color,
+ obj.color,
)
# ===== Экшены логика =====
@admin.action(description=_("Сделать целодневными (очистить время)"))
- def make_all_day(self, request: HttpRequest, queryset: QuerySet[CalendarEvent]) -> None:
+ def make_all_day(
+ self, request: HttpRequest, queryset: QuerySet[CalendarEvent]
+ ) -> None:
"""Делает выбранные события целодневными.
Args:
@@ -218,10 +250,14 @@ class CalendarEventAdmin(admin.ModelAdmin):
queryset: Выбранные события.
"""
updated = queryset.update(start_time=None, end_time=None, all_day=True)
- self.message_user(request, _(f"Обновлено событий: {updated}"), level=messages.SUCCESS)
+ self.message_user(
+ request, _(f"Обновлено событий: {updated}"), level=messages.SUCCESS
+ )
@admin.action(description=_("Очистить время (оставить даты как есть)"))
- def clear_time(self, request: HttpRequest, queryset: QuerySet[CalendarEvent]) -> None:
+ def clear_time(
+ self, request: HttpRequest, queryset: QuerySet[CalendarEvent]
+ ) -> None:
"""Очищает время начала/окончания у выбранных событий.
Args:
@@ -229,22 +265,30 @@ class CalendarEventAdmin(admin.ModelAdmin):
queryset: Выбранные события.
"""
updated = queryset.update(start_time=None, end_time=None)
- self.message_user(request, _(f"Очищено время у событий: {updated}"), level=messages.SUCCESS)
+ self.message_user(
+ request, _(f"Очищено время у событий: {updated}"), level=messages.SUCCESS
+ )
@admin.action(description=_("Пометить как ежегодные"))
- def set_annual(self, request: HttpRequest, queryset: QuerySet[CalendarEvent]) -> None:
+ def set_annual(
+ self, request: HttpRequest, queryset: QuerySet[CalendarEvent]
+ ) -> None:
"""Ставит повторяемость 'Ежегодно' у выбранных событий."""
updated = queryset.update(recurrence=Recurrence.ANNUAL)
self.message_user(request, _(f"Обновлено: {updated}"), level=messages.SUCCESS)
@admin.action(description=_("Пометить как одноразовые"))
- def set_one_time(self, request: HttpRequest, queryset: QuerySet[CalendarEvent]) -> None:
+ def set_one_time(
+ self, request: HttpRequest, queryset: QuerySet[CalendarEvent]
+ ) -> None:
"""Ставит повторяемость 'Одноразовое' у выбранных событий."""
updated = queryset.update(recurrence=Recurrence.ONE_TIME)
self.message_user(request, _(f"Обновлено: {updated}"), level=messages.SUCCESS)
# ===== Сохранение =====
- def save_model(self, request: HttpRequest, obj: CalendarEvent, form, change: bool) -> None:
+ def save_model(
+ self, request: HttpRequest, obj: CalendarEvent, form, change: bool
+ ) -> None:
"""Проставляет автора при первом сохранении.
Args:
@@ -260,4 +304,3 @@ class CalendarEventAdmin(admin.ModelAdmin):
# Добавляем инлайн подписок в админку календаря после импорта
CalendarAdmin.inlines = [CalendarSubscriptionInline]
-
diff --git a/backend/calendar_app/admin_calendar.py b/backend/calendar_app/admin_calendar.py
index 2ce6813b..11c217bd 100644
--- a/backend/calendar_app/admin_calendar.py
+++ b/backend/calendar_app/admin_calendar.py
@@ -53,32 +53,58 @@ class CalendarAdmin(admin.ModelAdmin):
readonly_fields = ("created_at", "updated_at", "event_count", "subscriber_count")
raw_id_fields = ("owner_user", "owner_department", "created_by")
fieldsets = (
- (_("Основное"), {
- "fields": ("title", "description", "icon"),
- }),
- (_("Владелец"), {
- "fields": ("owner_user", "owner_department"),
- "description": _("Указывается либо пользователь, либо отдел"),
- }),
- (_("Видимость"), {
- "fields": ("visibility",),
- }),
- (_("Отображение"), {
- "fields": ("color", "sort_order"),
- }),
- (_("Автоподписка"), {
- "fields": (
- "auto_subscribe_new_users",
- "auto_subscribe_department_members",
- ),
- }),
- (_("Статус"), {
- "fields": ("is_active",),
- }),
- (_("Статистика"), {
- "fields": ("event_count", "subscriber_count", "created_at", "updated_at"),
- "classes": ("collapse",),
- }),
+ (
+ _("Основное"),
+ {
+ "fields": ("title", "description", "icon"),
+ },
+ ),
+ (
+ _("Владелец"),
+ {
+ "fields": ("owner_user", "owner_department"),
+ "description": _("Указывается либо пользователь, либо отдел"),
+ },
+ ),
+ (
+ _("Видимость"),
+ {
+ "fields": ("visibility",),
+ },
+ ),
+ (
+ _("Отображение"),
+ {
+ "fields": ("color", "sort_order"),
+ },
+ ),
+ (
+ _("Автоподписка"),
+ {
+ "fields": (
+ "auto_subscribe_new_users",
+ "auto_subscribe_department_members",
+ ),
+ },
+ ),
+ (
+ _("Статус"),
+ {
+ "fields": ("is_active",),
+ },
+ ),
+ (
+ _("Статистика"),
+ {
+ "fields": (
+ "event_count",
+ "subscriber_count",
+ "created_at",
+ "updated_at",
+ ),
+ "classes": ("collapse",),
+ },
+ ),
)
@admin.display(description=_("Видимость"), ordering="visibility")
@@ -129,6 +155,5 @@ class CalendarAdmin(admin.ModelAdmin):
# Проставляем created_by при создании
if not change and obj.created_by_id is None:
obj.created_by = request.user
-
- super().save_model(request, obj, form, change)
+ super().save_model(request, obj, form, change)
diff --git a/backend/calendar_app/admin_subscription.py b/backend/calendar_app/admin_subscription.py
index c42ef6d8..6e5aaebe 100644
--- a/backend/calendar_app/admin_subscription.py
+++ b/backend/calendar_app/admin_subscription.py
@@ -49,30 +49,45 @@ class CalendarSubscriptionAdmin(admin.ModelAdmin):
readonly_fields = ("subscribed_at",)
raw_id_fields = ("user", "calendar")
fieldsets = (
- (_("Подписка"), {
- "fields": ("user", "calendar"),
- }),
- (_("Отображение"), {
- "fields": ("is_visible", "color_override"),
- }),
- (_("Права доступа"), {
- "fields": ("can_edit", "can_manage"),
- "description": _(
- "can_edit - может создавать/редактировать события; "
- "can_manage - может управлять календарем и правами"
- ),
- }),
- (_("Уведомления"), {
- "fields": (
- "notifications_enabled",
- "notify_on_new_event",
- "notify_on_event_change",
- "notify_on_event_delete",
- ),
- }),
- (_("Служебное"), {
- "fields": ("subscribed_at",),
- }),
+ (
+ _("Подписка"),
+ {
+ "fields": ("user", "calendar"),
+ },
+ ),
+ (
+ _("Отображение"),
+ {
+ "fields": ("is_visible", "color_override"),
+ },
+ ),
+ (
+ _("Права доступа"),
+ {
+ "fields": ("can_edit", "can_manage"),
+ "description": _(
+ "can_edit - может создавать/редактировать события; "
+ "can_manage - может управлять календарем и правами"
+ ),
+ },
+ ),
+ (
+ _("Уведомления"),
+ {
+ "fields": (
+ "notifications_enabled",
+ "notify_on_new_event",
+ "notify_on_event_change",
+ "notify_on_event_delete",
+ ),
+ },
+ ),
+ (
+ _("Служебное"),
+ {
+ "fields": ("subscribed_at",),
+ },
+ ),
)
@admin.display(description=_("Пользователь"), ordering="user__username")
@@ -115,9 +130,7 @@ class CalendarSubscriptionAdmin(admin.ModelAdmin):
if obj.can_manage and not obj.can_edit:
from django.core.exceptions import ValidationError
- raise ValidationError(
- _("can_manage требует can_edit=True")
- )
+ raise ValidationError(_("can_manage требует can_edit=True"))
super().save_model(request, obj, form, change)
diff --git a/backend/calendar_app/models.py b/backend/calendar_app/models.py
index 870b6eb5..4c3f06fa 100644
--- a/backend/calendar_app/models.py
+++ b/backend/calendar_app/models.py
@@ -26,6 +26,7 @@ class Recurrence(models.TextChoices):
class CalendarVisibility(models.TextChoices):
"""Видимость календаря."""
+
PUBLIC = "public", _("Публичный (все видят)")
DEPARTMENT = "department", _("Отдел (только члены отдела)")
PRIVATE = "private", _("Приватный (только владелец)")
@@ -34,57 +35,59 @@ class CalendarVisibility(models.TextChoices):
class CalendarManager(models.Manager):
"""Менеджер для модели Calendar с дополнительными методами."""
-
+
def get_available_for_user(self, user):
"""Возвращает календари, доступные для просмотра пользователю."""
if user.is_superuser or user.is_staff:
# Админы видят все активные календари
return self.filter(is_active=True)
-
+
# Публичные календари
q = Q(visibility=CalendarVisibility.PUBLIC, is_active=True)
-
+
# Календари, где пользователь владелец
q |= Q(owner_user=user, is_active=True)
-
+
# Календари отделов, где пользователь член
- user_departments = user.departments_links.filter(
- is_active=True
- ).values_list("department_id", flat=True)
-
+ user_departments = user.departments_links.filter(is_active=True).values_list(
+ "department_id", flat=True
+ )
+
q |= Q(
owner_department_id__in=user_departments,
visibility=CalendarVisibility.DEPARTMENT,
- is_active=True
+ is_active=True,
)
-
+
# Календари с подпиской пользователя
subscribed_calendar_ids = user.calendar_subscriptions.values_list(
"calendar_id", flat=True
)
q |= Q(id__in=subscribed_calendar_ids, is_active=True)
-
+
return self.filter(q).distinct()
class Calendar(models.Model):
"""Настраиваемый календарь (опциональный, расширенная функциональность).
-
+
Если событие создано БЕЗ calendar → работает старая логика (department/employee).
Если событие создано С calendar → игнорируются department/employee, используются настройки календаря.
"""
-
+
# Основное
title = models.CharField(_("Название"), max_length=200)
description = models.TextField(_("Описание"), blank=True)
- color = models.CharField(_("Цвет"), max_length=7, default="#0d6efd", help_text="#RRGGBB")
+ color = models.CharField(
+ _("Цвет"), max_length=7, default="#0d6efd", help_text="#RRGGBB"
+ )
icon = models.CharField(
_("Иконка"),
max_length=50,
blank=True,
- help_text=_("Bootstrap icon, например: calendar-event")
+ help_text=_("Bootstrap icon, например: calendar-event"),
)
-
+
# Владение (опциональное)
owner_user = models.ForeignKey(
User,
@@ -95,7 +98,7 @@ class Calendar(models.Model):
verbose_name=_("Владелец"),
help_text=_("Если задано — личный календарь пользователя"),
)
-
+
owner_department = models.ForeignKey(
"employees.Department",
on_delete=models.CASCADE,
@@ -105,7 +108,7 @@ class Calendar(models.Model):
verbose_name=_("Отдел-владелец"),
help_text=_("Если задано — календарь отдела"),
)
-
+
# Настройки видимости
visibility = models.CharField(
_("Видимость"),
@@ -113,27 +116,27 @@ class Calendar(models.Model):
choices=CalendarVisibility.choices,
default=CalendarVisibility.CUSTOM,
)
-
+
# Права по умолчанию для новых подписчиков
default_can_edit = models.BooleanField(
_("Могут редактировать по умолчанию"),
default=False,
help_text=_("Если True, все подписчики могут создавать/редактировать события"),
)
-
+
# Автоподписка
auto_subscribe_new_users = models.BooleanField(
_("Автоподписка для новых пользователей"),
default=False,
help_text=_("Автоматически подписывать новых сотрудников"),
)
-
+
auto_subscribe_department_members = models.BooleanField(
_("Автоподписка для членов отдела"),
default=False,
help_text=_("Автоматически подписывать членов отдела-владельца"),
)
-
+
# Служебное
is_active = models.BooleanField(_("Активен"), default=True)
created_by = models.ForeignKey(
@@ -146,9 +149,9 @@ class Calendar(models.Model):
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
-
+
objects = CalendarManager()
-
+
class Meta:
verbose_name = _("Календарь")
verbose_name_plural = _("Календари")
@@ -158,126 +161,132 @@ class Calendar(models.Model):
models.Index(fields=["owner_department", "is_active"]),
models.Index(fields=["visibility"]),
]
-
+
def __str__(self):
if self.owner_user:
return f"{self.title} ({self.owner_user.username})"
if self.owner_department:
return f"{self.title} ({self.owner_department.name})"
return f"{self.title} (Глобальный)"
-
+
def clean(self):
# Нельзя одновременно user и department
if self.owner_user and self.owner_department:
raise ValidationError(
_("Календарь не может одновременно принадлежать пользователю и отделу.")
)
-
+
@property
def is_global(self):
"""Глобальный календарь (без владельца)."""
return not self.owner_user_id and not self.owner_department_id
-
+
@property
def is_personal(self):
"""Личный календарь пользователя."""
return bool(self.owner_user_id)
-
+
@property
def is_department(self):
"""Календарь отдела."""
return bool(self.owner_department_id)
-
+
def is_owner(self, user):
"""Проверяет, является ли пользователь владельцем календаря."""
if self.owner_user_id:
return self.owner_user_id == user.id
if self.owner_department_id:
# Владелец отдела = руководитель отдела
- return self.owner_department.head_id == user.id if hasattr(self.owner_department, 'head_id') else False
+ return (
+ self.owner_department.head_id == user.id
+ if hasattr(self.owner_department, "head_id")
+ else False
+ )
# Глобальный календарь — владелец только админы
return user.is_superuser or user.is_staff
-
+
def can_user_view(self, user):
"""Проверяет, может ли пользователь просматривать календарь."""
if not self.is_active:
return False
-
+
if user.is_superuser or user.is_staff:
return True
-
+
# Владелец всегда может просматривать
if self.is_owner(user):
return True
-
+
# Публичный календарь
if self.visibility == CalendarVisibility.PUBLIC:
return True
-
+
# Календарь отдела
- if self.visibility == CalendarVisibility.DEPARTMENT and self.owner_department_id:
+ if (
+ self.visibility == CalendarVisibility.DEPARTMENT
+ and self.owner_department_id
+ ):
# Проверяем, является ли пользователь членом отдела
return user.departments_links.filter(
- department_id=self.owner_department_id,
- is_active=True
+ department_id=self.owner_department_id, is_active=True
).exists()
-
+
# Приватный или настраиваемый — проверяем подписку
if self.visibility in [CalendarVisibility.PRIVATE, CalendarVisibility.CUSTOM]:
return self.subscriptions.filter(user=user).exists()
-
+
return False
-
+
def can_user_edit(self, user):
"""Проверяет, может ли пользователь редактировать события в календаре."""
if user.is_superuser or user.is_staff:
return True
-
+
# Владелец всегда может редактировать
if self.is_owner(user):
return True
-
+
# Проверяем подписку с правом редактирования
subscription = self.subscriptions.filter(user=user).first()
if subscription:
return subscription.can_edit
-
+
return False
-
+
def can_user_manage(self, user):
"""Проверяет, может ли пользователь управлять календарем."""
if user.is_superuser or user.is_staff:
return True
-
+
# Владелец всегда может управлять
if self.is_owner(user):
return True
-
+
# Проверяем подписку с правом управления
subscription = self.subscriptions.filter(user=user).first()
if subscription:
return subscription.can_manage
-
+
return False
class CalendarSubscription(models.Model):
"""Подписка пользователя на календарь с настройками отображения."""
-
+
calendar = models.ForeignKey(
Calendar,
on_delete=models.CASCADE,
related_name="subscriptions",
verbose_name=_("Календарь"),
)
-
+
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="calendar_subscriptions",
verbose_name=_("Пользователь"),
)
-
+
# Настройки отображения
is_visible = models.BooleanField(_("Отображать"), default=True)
color_override = models.CharField(
@@ -287,26 +296,30 @@ class CalendarSubscription(models.Model):
null=True,
help_text=_("Переопределяет цвет календаря"),
)
-
+
# Права подписчика
can_edit = models.BooleanField(
_("Может редактировать"),
default=False,
help_text=_("Может создавать/редактировать события в этом календаре"),
)
-
+
can_manage = models.BooleanField(
_("Может управлять"),
default=False,
help_text=_("Может управлять правами других пользователей"),
)
-
+
# Уведомления
- notify_on_new_event = models.BooleanField(_("Уведомлять о новых событиях"), default=True)
- notify_on_event_change = models.BooleanField(_("Уведомлять об изменениях"), default=True)
-
+ notify_on_new_event = models.BooleanField(
+ _("Уведомлять о новых событиях"), default=True
+ )
+ notify_on_event_change = models.BooleanField(
+ _("Уведомлять об изменениях"), default=True
+ )
+
subscribed_at = models.DateTimeField(auto_now_add=True)
-
+
class Meta:
verbose_name = _("Подписка на календарь")
verbose_name_plural = _("Подписки на календари")
@@ -315,18 +328,18 @@ class CalendarSubscription(models.Model):
models.Index(fields=["user", "is_visible"]),
models.Index(fields=["calendar", "can_edit"]),
]
-
+
def __str__(self):
return f"{self.user.username} → {self.calendar.title}"
class CalendarEvent(models.Model):
"""Событие календаря с поддержкой повторяемости.
-
+
ОБРАТНАЯ СОВМЕСТИМОСТЬ:
- Если calendar = NULL → используется старая логика (department/employee)
- Если calendar задан → игнорируются department/employee
-
+
LEGACY (старая логика):
- department = NULL и employee = NULL → событие компании (глобальное)
- department задан и employee = NULL → событие отдела
@@ -342,11 +355,10 @@ class CalendarEvent(models.Model):
blank=True,
verbose_name=_("Календарь"),
help_text=_(
- "Если не задан — используется стандартная логика "
- "(department/employee)"
+ "Если не задан — используется стандартная логика (department/employee)"
),
)
-
+
# ✅ LEGACY: Старые поля для обратной совместимости
department = models.ForeignKey(
"employees.Department",
@@ -357,7 +369,7 @@ class CalendarEvent(models.Model):
blank=True,
help_text=_("LEGACY: используется если calendar=NULL"),
)
-
+
employee = models.ForeignKey(
User,
on_delete=models.CASCADE,
@@ -467,11 +479,8 @@ class CalendarEvent(models.Model):
def __str__(self) -> str:
# НОВОЕ: Приоритет calendar
if self.calendar_id:
- return (
- f"[{self.calendar.title}] {self.title} "
- f"({self.start_date:%d.%m.%Y})"
- )
-
+ return f"[{self.calendar.title}] {self.title} ({self.start_date:%d.%m.%Y})"
+
# LEGACY: Старая логика
if self.employee_id:
scope = f"Личный ({self.employee})"
@@ -479,7 +488,7 @@ class CalendarEvent(models.Model):
scope = str(self.department)
else:
scope = _("Компания")
-
+
if self.end_date:
return (
f"{scope}: {self.title} "
@@ -491,7 +500,7 @@ class CalendarEvent(models.Model):
def is_legacy_event(self) -> bool:
"""True если событие использует старую логику (без calendar)."""
return self.calendar_id is None
-
+
@property
def is_modern_event(self) -> bool:
"""True если событие использует новую логику (с calendar)."""
@@ -501,11 +510,11 @@ class CalendarEvent(models.Model):
def is_company(self) -> bool:
"""True если событие глобальное (LEGACY: без отдела и сотрудника)."""
return (
- self.calendar_id is None and
- self.department_id is None and
- self.employee_id is None
+ self.calendar_id is None
+ and self.department_id is None
+ and self.employee_id is None
)
-
+
@property
def is_personal(self) -> bool:
"""True если событие личное (LEGACY или calendar.is_personal)."""
@@ -534,17 +543,14 @@ class CalendarEvent(models.Model):
"department или employee."
)
)
-
+
# LEGACY: Проверка взаимоисключающих полей (только если calendar=NULL)
if not self.calendar_id:
if self.department_id and self.employee_id:
raise ValidationError(
- _(
- "Событие не может одновременно принадлежать "
- "отделу и сотруднику."
- )
+ _("Событие не может одновременно принадлежать отделу и сотруднику.")
)
-
+
# База дат
if self.end_date and self.start_date and self.end_date < self.start_date:
raise ValidationError(
diff --git a/backend/static/css/calendar-improvements.css b/backend/static/css/calendar-improvements.css
new file mode 100644
index 00000000..527a6836
--- /dev/null
+++ b/backend/static/css/calendar-improvements.css
@@ -0,0 +1,289 @@
+/* ========================================
+ Улучшенные стили для календаря
+ ======================================== */
+
+/* Улучшенное отображение дней с событиями */
+.fc-daygrid-day {
+ position: relative;
+}
+
+/* Современный стиль для ячеек календаря */
+.fc-daygrid-day-frame {
+ min-height: 100px;
+ position: relative;
+}
+
+/* Убираем стандартные рамки событий и делаем их более гладкими */
+.fc-event {
+ border-radius: 4px !important;
+ margin: 1px 2px !important;
+ padding: 2px 4px !important;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ position: relative;
+}
+
+/* Улучшенное отображение текста события */
+.fc-event-title {
+ font-weight: 500;
+ padding: 1px 0;
+}
+
+/* Стиль для дней с большим количеством событий */
+.fc-daygrid-day.fc-day-today {
+ background-color: rgba(13, 110, 253, 0.05) !important;
+}
+
+.fc-daygrid-day.fc-day-today .fc-daygrid-day-number {
+ background-color: #0d6efd;
+ color: white;
+ border-radius: 50%;
+ width: 24px;
+ height: 24px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 600;
+}
+
+/* Улучшенный popover для множества событий */
+.fc-popover {
+ border-radius: 8px !important;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.15) !important;
+ border: 1px solid rgba(0,0,0,0.1) !important;
+ max-width: 300px;
+}
+
+.fc-popover-header {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
+ color: white !important;
+ padding: 8px 12px !important;
+ border-radius: 8px 8px 0 0 !important;
+ font-weight: 600;
+}
+
+.fc-popover-body {
+ padding: 8px !important;
+ max-height: 400px;
+ overflow-y: auto;
+}
+
+/* Стили для ссылки "+N еще" */
+.fc-more-link {
+ color: #0d6efd !important;
+ font-weight: 600 !important;
+ font-size: 11px !important;
+ padding: 2px 6px !important;
+ border-radius: 3px !important;
+ background: rgba(13, 110, 253, 0.1) !important;
+ transition: all 0.2s ease;
+}
+
+.fc-more-link:hover {
+ background: rgba(13, 110, 253, 0.2) !important;
+ text-decoration: none !important;
+}
+
+/* Улучшенное отображение времени события */
+.fc-event-time {
+ font-weight: 400;
+ font-size: 10px;
+ opacity: 0.9;
+ margin-right: 3px;
+}
+
+/* Стиль для событий с левой цветной границей (календарь) */
+.fc-event.has-calendar-color {
+ padding-left: 8px !important;
+}
+
+/* Анимация появления событий */
+@keyframes fadeInEvent {
+ from {
+ opacity: 0;
+ transform: translateY(-5px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.fc-event {
+ animation: fadeInEvent 0.3s ease;
+}
+
+/* Улучшенный hover для всех событий */
+.fc-event:hover {
+ z-index: 999 !important;
+ transform: translateY(-2px) scale(1.02);
+}
+
+/* Стиль для выходных дней */
+.fc-day-sat,
+.fc-day-sun {
+ background-color: rgba(220, 53, 69, 0.02);
+}
+
+/* Улучшенная читаемость заголовков дней недели */
+.fc-col-header-cell {
+ font-weight: 600;
+ background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
+ border-color: #dee2e6 !important;
+}
+
+/* Современный стиль для кнопок навигации календаря */
+.fc-button {
+ border-radius: 6px !important;
+ padding: 6px 12px !important;
+ font-weight: 500 !important;
+ transition: all 0.2s ease !important;
+}
+
+.fc-button:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important;
+}
+
+/* Стиль для заголовка календаря */
+.fc-toolbar-title {
+ font-weight: 600 !important;
+ font-size: 1.25rem !important;
+ color: #2c3e50;
+}
+
+/* Улучшенное отображение номера дня */
+.fc-daygrid-day-number {
+ padding: 4px 6px;
+ font-weight: 500;
+ color: #495057;
+}
+
+/* Стиль для прошедших дней */
+.fc-day-past:not(.fc-day-today) {
+ background-color: rgba(0,0,0,0.02);
+}
+
+.fc-day-past:not(.fc-day-today) .fc-daygrid-day-number {
+ color: #adb5bd;
+}
+
+/* Улучшенное отображение границ между днями */
+.fc-daygrid-day-frame {
+ border: 1px solid #e9ecef !important;
+}
+
+/* Стиль для событий на весь день */
+.fc-daygrid-event.fc-daygrid-block-event {
+ padding: 3px 6px !important;
+}
+
+/* Адаптивность для мобильных */
+@media (max-width: 768px) {
+ .fc-daygrid-day-frame {
+ min-height: 80px;
+ }
+
+ .fc-event {
+ font-size: 10px !important;
+ padding: 1px 3px !important;
+ }
+
+ .fc-toolbar-title {
+ font-size: 1rem !important;
+ }
+}
+
+/* Улучшенная прокрутка в popover */
+.fc-popover-body::-webkit-scrollbar {
+ width: 6px;
+}
+
+.fc-popover-body::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 3px;
+}
+
+.fc-popover-body::-webkit-scrollbar-thumb {
+ background: #888;
+ border-radius: 3px;
+}
+
+.fc-popover-body::-webkit-scrollbar-thumb:hover {
+ background: #555;
+}
+
+/* Индикатор загрузки событий */
+.fc-view-harness.fc-view-harness-active {
+ position: relative;
+}
+
+/* Стиль для событий разной продолжительности */
+.fc-event.fc-event-start.fc-event-end {
+ /* Событие в один день - закругленные углы */
+ border-radius: 4px !important;
+}
+
+.fc-event.fc-event-start:not(.fc-event-end) {
+ /* Начало многодневного события - закругление слева */
+ border-radius: 4px 0 0 4px !important;
+}
+
+.fc-event:not(.fc-event-start).fc-event-end {
+ /* Конец многодневного события - закругление справа */
+ border-radius: 0 4px 4px 0 !important;
+}
+
+.fc-event:not(.fc-event-start):not(.fc-event-end) {
+ /* Середина многодневного события - без закруглений */
+ border-radius: 0 !important;
+}
+
+/* Улучшенная видимость выбранной даты при создании события */
+.fc-highlight {
+ background: rgba(13, 110, 253, 0.15) !important;
+ border-radius: 4px;
+}
+
+/* Стиль для недели */
+.week-strip-header {
+ font-weight: 600;
+ color: #495057;
+ padding: 12px 0 8px;
+ border-top: 1px solid #e9ecef;
+ margin-top: 16px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.week-vertical {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ margin-top: 8px;
+}
+
+/* Улучшенный стиль для списка календарей */
+.calendar-list-container {
+ scrollbar-width: thin;
+ scrollbar-color: #cbd5e0 #f7fafc;
+}
+
+.calendar-list-container::-webkit-scrollbar {
+ width: 6px;
+}
+
+.calendar-list-container::-webkit-scrollbar-track {
+ background: #f7fafc;
+ border-radius: 3px;
+}
+
+.calendar-list-container::-webkit-scrollbar-thumb {
+ background: #cbd5e0;
+ border-radius: 3px;
+}
+
+.calendar-list-container::-webkit-scrollbar-thumb:hover {
+ background: #a0aec0;
+}
diff --git a/backend/static/js/api/calendarsApi.js b/backend/static/js/api/calendarsApi.js
index 241cd806..744ff841 100644
--- a/backend/static/js/api/calendarsApi.js
+++ b/backend/static/js/api/calendarsApi.js
@@ -3,7 +3,7 @@
* @module api/calendarsApi
*/
-import { dataManager } from '../managers/dataManager.js';
+import { dataManager } from "../managers/dataManager.js";
/**
* Получить токен авторизации
@@ -12,13 +12,13 @@ import { dataManager } from '../managers/dataManager.js';
*/
function getAccessToken() {
const meta = document.querySelector('meta[name="api-access"]');
- const token = meta?.getAttribute('content')?.trim();
+ const token = meta?.getAttribute("content")?.trim();
if (token) return token;
-
+
try {
- return localStorage.getItem('api.access') || '';
+ return localStorage.getItem("api.access") || "";
} catch {
- return '';
+ return "";
}
}
@@ -28,9 +28,12 @@ function getAccessToken() {
* @private
*/
function getCsrfToken() {
- return document.querySelector('[name=csrfmiddlewaretoken]')?.value ||
- document.querySelector('meta[name="csrf-token"]')?.content ||
- getCookie('csrftoken') || '';
+ return (
+ document.querySelector("[name=csrfmiddlewaretoken]")?.value ||
+ document.querySelector('meta[name="csrf-token"]')?.content ||
+ getCookie("csrftoken") ||
+ ""
+ );
}
/**
@@ -42,8 +45,8 @@ function getCsrfToken() {
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
- if (parts.length === 2) return parts.pop().split(';').shift();
- return '';
+ if (parts.length === 2) return parts.pop().split(";").shift();
+ return "";
}
/**
@@ -55,15 +58,15 @@ function getCookie(name) {
function authHeaders(includeContentType = true) {
const token = getAccessToken();
const headers = {
- 'Accept': 'application/json',
- ...(token ? { 'Authorization': `Bearer ${token}` } : {}),
- 'X-CSRFToken': getCsrfToken()
+ Accept: "application/json",
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ "X-CSRFToken": getCsrfToken(),
};
-
+
if (includeContentType) {
- headers['Content-Type'] = 'application/json';
+ headers["Content-Type"] = "application/json";
}
-
+
return headers;
}
@@ -73,30 +76,33 @@ function authHeaders(includeContentType = true) {
* @returns {Promise