Corp/backend/static/js/README_ARCHITECTURE.md
Игорь Надеин 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

18 KiB
Raw Blame History

Архитектура JavaScript модулей календарной системы

📁 Структура директорий

static/js/
├── api/                        # API обёртки с кешированием
│   ├── calendarApi.js         # События календаря (GET)
│   └── calendarsApi.js        # CRUD календарей + подписки
│
├── components/                 # UI компоненты
│   ├── calendarWidget/        # Модули основного виджета (планируется)
│   ├── calendarWidget.js      # Основной виджет FullCalendar
│   ├── calendarWidgetIntegration.js  # Интеграция виджета с менеджером
│   ├── calendarManager.js     # Управление списком календарей
│   └── calendarManageModal.js # Модальное окно CRUD календаря
│
├── constants/                  # Константы и энумы
│   ├── calendarTypes.js       # Типы календарей, цвета
│   └── apiUrls.js            # URL эндпоинтов API
│
├── utils/                     # Утилитные функции
│   ├── authUtils.js          # Авторизация (токены, headers)
│   ├── dateUtils.js          # Работа с датами
│   └── calendarTypeResolver.js # Определение типа календаря
│
└── managers/                  # Менеджеры (уже существуют)
    └── dataManager.js        # Кеширование данных

🔧 Модули

API Layer (api/)

calendarApi.js

Назначение: Работа с событиями календаря (чтение)

Экспорт:

  • getCalendarEvents(params, ttl) - получить события
  • getCalendarEvent(eventId, ttl) - получить событие по ID
  • invalidateCalendarEvents(params) - инвалидировать кеш
  • preloadCalendarEvents(params, events) - предзагрузка

Зависимости:

import { dataManager } from '../managers/dataManager.js';
import { authHeaders } from '../utils/authUtils.js';
import { API_URLS, API_DEFAULTS } from '../constants/apiUrls.js';

calendarsApi.js

Назначение: CRUD операции с календарями + подписки

Экспорт:

  • getMyCalendars(ttl) - получить мои календари
  • getCalendar(calendarId, ttl) - получить календарь по ID
  • createCalendar(calendarData) - создать календарь
  • updateCalendar(calendarId, updates) - обновить календарь
  • deleteCalendar(calendarId) - удалить календарь
  • subscribeToCalendar(calendarId, options) - подписаться
  • unsubscribeFromCalendar(calendarId) - отписаться
  • invalidateCalendarsCache() - инвалидировать кеш

Зависимости:

import { dataManager } from '../managers/dataManager.js';
import { authHeaders, getCsrfToken } from '../utils/authUtils.js';
import { API_URLS, API_DEFAULTS } from '../constants/apiUrls.js';

Constants Layer (constants/)

calendarTypes.js

Назначение: Типы календарей, цвета, утилиты

Экспорт:

// Енумы
export const CALENDAR_TYPES = {
  LEGACY_COMPANY: 'legacy-company',
  LEGACY_PERSONAL: 'legacy-personal',
  LEGACY_DEPT_PREFIX: 'legacy-dept-',
  COMPANY: 'company',
  PERSONAL: 'personal',
  DEPARTMENT: 'department',
  PUBLIC: 'public',
  PRIVATE: 'private',
  CUSTOM: 'custom',
};

export const CALENDAR_COLORS = {
  COMPANY: '#dc3545',
  PERSONAL: '#0d6efd',
  DEPARTMENT: '#198754',
  PUBLIC: '#6c757d',
  DEFAULT: '#0d6efd',
};

// Утилиты
export function isLegacyCalendar(id)
export function getLegacyCalendarType(id)
export function extractDepartmentId(legacyId)
export function createLegacyDeptId(departmentId)

apiUrls.js

Назначение: URL эндпоинтов API, дефолтные параметры

Экспорт:

export const API_URLS = {
  EVENTS: '/api/v1/calendar/events/',
  EVENT_DETAIL: (id) => `/api/v1/calendar/events/${id}/`,
  CALENDARS: '/api/v1/calendar/calendars/',
  CALENDAR_DETAIL: (id) => `/api/v1/calendar/calendars/${id}/`,
  CALENDAR_SUBSCRIBE: (id) => `/api/v1/calendar/calendars/${id}/subscribe/`,
  CALENDAR_UNSUBSCRIBE: (id) => `/api/v1/calendar/calendars/${id}/unsubscribe/`,
  MY_DEPARTMENTS: '/api/v1/departments/my-departments/',
};

export const API_DEFAULTS = {
  TTL: {
    EVENTS: 30000,        // 30 секунд
    EVENT_DETAIL: 60000,  // 1 минута
    CALENDARS: 60000,     // 1 минута
    DEPARTMENTS: 300000,  // 5 минут
  },
  TIMEOUT: {
    DEFAULT: 10000,
    UPLOAD: 30000,
  },
};

export function getApiUrl(path, baseUrl)

Utils Layer (utils/)

authUtils.js

Назначение: Работа с токенами и авторизацией

Экспорт:

export function getAccessToken()        // Получить Bearer токен
export function getCsrfToken()          // Получить CSRF токен
export function getCookie(name)         // Получить cookie
export function authHeaders(includeContentType) // Заголовки для API
export function hasValidToken()         // Проверка наличия токена

Использование:

import { authHeaders } from '../utils/authUtils.js';

const response = await fetch('/api/v1/events/', {
  headers: authHeaders()
});

dateUtils.js

Назначение: Работа с датами и временем

Экспорт:

export function formatDate(date)              // YYYY-MM-DD
export function ymdLocal(date)                // YYYY-MM-DD (локальное время)
export function fmtDate(date)                 // ДД.ММ.ГГГГ
export function fmtTime(date)                 // ЧЧ:ММ
export function formatEventPeriod(event)      // Период события
export function addDateRangeToUrl(url, start, end)
export function parseDate(dateStr)            // Парсинг даты
export function startOfDay(date)              // Начало дня
export function endOfDay(date)                // Конец дня

Использование:

import { formatDate, formatEventPeriod } from '../utils/dateUtils.js';

const startStr = formatDate(new Date());
const period = formatEventPeriod(event);

calendarTypeResolver.js

Назначение: Определение типа календаря и параметров запросов

Экспорт:

export function resolveCalendarParams(calendarId, baseParams)
  // Определить параметры для API запроса

export function resolveEventPayload(calendarId, eventData)
  // Определить payload для создания/обновления события

export function getCalendarTypeName(calendarId, calendars)
  // Получить человекочитаемое имя типа

export function canEditCalendar(calendarId, calendars)
  // Проверить права на редактирование

export function isValidCalendarId(calendarId)
  // Валидация ID календаря

Использование:

import { resolveCalendarParams, resolveEventPayload } from '../utils/calendarTypeResolver.js';

// Загрузка событий
const params = resolveCalendarParams(calendarId, { start, end });
const events = await getCalendarEvents(params);

// Создание события
const payload = resolveEventPayload(calendarId, eventData);
await createEvent(payload);

Components Layer (components/)

calendarWidget.js (1821 строка)

Статус: 🔴 Требует рефакторинга

Планируется декомпозиция на:

components/calendarWidget/
├── index.js                  # Главный экспорт + инициализация
├── fullcalendarConfig.js     # Конфигурация FullCalendar
├── eventHandlers.js          # Submit, click, context menu
├── eventRendering.js         # eventDidMount, eventContent
├── weekListRenderer.js       # renderVertical, updateWeekLists
├── formHelpers.js            # syncByRecurrence, initColorPicker
└── permissions.js            # checkEventPermissions

calendarWidgetIntegration.js (340 строк)

Статус: ✅ Хорошо

Назначение: Связка calendarWidget с calendarManager

Зависимости:

import { initCalendarManager } from "./calendarManager.js";
import { initCalendarManageModal } from "./calendarManageModal.js";
import { getCalendarEvents } from "../api/calendarApi.js";
import { formatDate } from "../utils/dateUtils.js";
import { resolveCalendarParams } from "../utils/calendarTypeResolver.js";

calendarManager.js (398 строк)

Статус: ✅ Отлично

Назначение: UI-компонент для списка календарей

Зависимости:

import {
  getMyCalendars,
  subscribeToCalendar,
  unsubscribeFromCalendar,
  invalidateCalendarsCache,
} from "../api/calendarsApi.js";
import {
  CALENDAR_TYPES,
  CALENDAR_COLORS,
  createLegacyDeptId,
} from "../constants/calendarTypes.js";

calendarManageModal.js (290 строк)

Статус: ✅ Эталон качества

Назначение: Модальное окно создания/редактирования календаря


🔄 Миграция и обратная совместимость

Legacy система

Поддержка старой системы календарей через legacy ID:

  • legacy-company - корпоративный календарь
  • legacy-personal - личный календарь
  • legacy-dept-{id} - календарь отдела

Новая система

Календари с числовым ID из модели Calendar:

  • 123 - новый календарь (number)

Автоматическое определение

import { resolveCalendarParams } from '../utils/calendarTypeResolver.js';

// Автоматически определяет тип и добавляет нужные параметры
const params = resolveCalendarParams(calendarId, { start, end });

// legacy-company → { start, end }
// legacy-personal → { start, end, employee_id }
// legacy-dept-5 → { start, end, department_id: 5 }
// 123 → { start, end, calendar_id: 123 }

📦 Импорты и зависимости

Правила импортов

  1. Всегда используйте относительные пути

    import { authHeaders } from '../utils/authUtils.js';
    
  2. Группируйте импорты

    // 1. Сторонние библиотеки (если есть)
    import { Calendar } from '@fullcalendar/core';
    
    // 2. API слой
    import { getCalendarEvents } from '../api/calendarApi.js';
    
    // 3. Константы
    import { CALENDAR_TYPES } from '../constants/calendarTypes.js';
    
    // 4. Утилиты
    import { authHeaders } from '../utils/authUtils.js';
    import { formatDate } from '../utils/dateUtils.js';
    
    // 5. Компоненты
    import { initCalendarManager } from './calendarManager.js';
    
  3. Не создавайте циклические зависимости

    • ❌ utils → components → utils
    • ✅ components → utils → constants

Граф зависимостей

┌─────────────────┐
│   constants/    │ ◄─── Базовый слой (нет зависимостей)
└────────┬────────┘
         │
┌────────▼────────┐
│     utils/      │ ◄─── Зависит только от constants
└────────┬────────┘
         │
┌────────▼────────┐
│   managers/     │ ◄─── Зависит от utils
└────────┬────────┘
         │
┌────────▼────────┐
│      api/       │ ◄─── Зависит от utils, constants, managers
└────────┬────────┘
         │
┌────────▼────────┐
│  components/    │ ◄─── Зависит от api, utils, constants
└─────────────────┘

🎯 Следующие шаги

P0 - Критично (Sprint 1)

  • ✅ Создать utils/authUtils.js
  • ✅ Создать utils/dateUtils.js
  • ✅ Создать constants/calendarTypes.js
  • ✅ Создать constants/apiUrls.js
  • ✅ Создать utils/calendarTypeResolver.js
  • ✅ Обновить api/calendarApi.js (использовать новые утилиты)
  • ✅ Обновить api/calendarsApi.js (использовать новые утилиты)
  • ✅ Обновить components/calendarWidgetIntegration.js
  • ✅ Обновить components/calendarManager.js
  • 🔧 Обновить components/calendarWidget.js (частично)
  • 🔧 Тестирование новой структуры

P1 - Важно (Sprint 2)

  • 📦 Разделить calendarWidget.js на модули
  • 📦 Создать components/calendarWidget/ директорию
  • 📦 Переместить eventDidMount логику в отдельный файл
  • 📦 Переместить form handlers в отдельный файл

P2 - Желательно (Sprint 3)

  • 📝 Добавить unit-тесты для utils
  • 📝 Добавить integration-тесты для API
  • 📝 TypeScript миграция (.js → .ts)

🧪 Тестирование

Ручное тестирование

  1. Проверка импортов

    # Проверить нет ли ошибок в консоли
    grep -r "import.*from" static/js/
    
  2. Проверка работы календаря

    • Открыть страницу с календарём
    • Проверить загрузку событий
    • Создать новое событие
    • Отредактировать событие
    • Удалить событие
  3. Проверка legacy режима

    • Переключить на legacy-company
    • Переключить на legacy-personal
    • Переключить на legacy-dept-X

Автоматическое тестирование (TODO)

// tests/utils/authUtils.test.js
import { authHeaders, getAccessToken } from '../utils/authUtils.js';

describe('authUtils', () => {
  it('should return headers with Bearer token', () => {
    const headers = authHeaders();
    expect(headers).toHaveProperty('Authorization');
  });
});

📝 Соглашения о коде

JSDoc комментарии

/**
 * Краткое описание функции
 * @param {Type} paramName - Описание параметра
 * @param {Type} [optionalParam] - Опциональный параметр
 * @returns {ReturnType} Описание возвращаемого значения
 * @throws {Error} Когда выбрасывается ошибка
 * @example
 * const result = myFunction('value');
 */
export function myFunction(paramName, optionalParam) {
  // ...
}

Именование

  • Функции: camelCase (getCalendarEvents, formatDate)
  • Константы: UPPER_SNAKE_CASE (CALENDAR_TYPES, API_URLS)
  • Приватные функции: начинаются с _ (если нужно)
  • Компоненты: PascalCase в названии файла (CalendarManager.js)

Файловая структура модуля

// 1. JSDoc для всего модуля
/**
 * @fileoverview Краткое описание модуля
 * @module путь/к/модулю
 */

// 2. Импорты (сгруппированные)
import { ... } from '...';

// 3. Приватные константы и функции
const PRIVATE_CONST = ...;
function _privateFunction() { ... }

// 4. Экспортируемые функции
export function publicFunction() { ... }

// 5. Дефолтный экспорт (если есть)
export default { ... };

🔍 Диагностика проблем

Ошибка: "Cannot find module"

Причина: Неправильный относительный путь импорта

Решение:

// ❌ Неправильно
import { authHeaders } from 'utils/authUtils.js';

// ✅ Правильно
import { authHeaders } from '../utils/authUtils.js';

Ошибка: "X is not defined"

Причина: Забыли импортировать функцию/константу

Решение:

// Добавить импорт
import { CALENDAR_TYPES } from '../constants/calendarTypes.js';

Ошибка: "Circular dependency"

Причина: Циклическая зависимость между модулями

Решение: Вынести общую логику в отдельный модуль


📚 Дополнительные ресурсы


Последнее обновление: 11 февраля 2026 Версия: 1.0.0 Автор: GitHub Copilot