From d9395def630634a1b83bec57995f1e275c84244c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 11 Feb 2026 10:10:30 +0300 Subject: [PATCH] feat(calendar): integrate calendar manager with widget (Phase 3 - Part 2) - Created calendarWidgetIntegration.js as integration layer - Modified calendarWidget.js to use new calendar filtering system - Added fallback to legacy mode for backward compatibility - Updated calendar_scripts.html to initialize integration - Implemented fetchEventsForVisibleCalendars with calendar_id filtering - Added event deduplication and color override from calendars - Exposed window.calendarIntegration for global access - Added comprehensive logging for debugging Technical decisions: - Non-invasive integration via optional chaining - Callback-based synchronization (onCalendarToggle, onCalendarsChange) - Parallel event loading with Promise.all() - Automatic legacy mode when no calendars configured --- .../static/js/components/calendarWidget.js | 13 +- .../components/calendarWidgetIntegration.js | 247 ++++++++++++++++++ .../includes/components/calendar_scripts.html | 11 + .../CALENDAR_OPTIONAL_ARCHITECTURE.md | 34 ++- docs/in-progress/PHASE3_PART2_INTEGRATION.md | 238 +++++++++++++++++ 5 files changed, 536 insertions(+), 7 deletions(-) create mode 100644 backend/static/js/components/calendarWidgetIntegration.js create mode 100644 docs/in-progress/PHASE3_PART2_INTEGRATION.md diff --git a/backend/static/js/components/calendarWidget.js b/backend/static/js/components/calendarWidget.js index 3b12578e..58b53abf 100644 --- a/backend/static/js/components/calendarWidget.js +++ b/backend/static/js/components/calendarWidget.js @@ -962,7 +962,18 @@ export function initCalendarWidget(options = {}) { selectable: true, // Позволяет выбирать даты events: async (info, success, failure) => { try { - const raw = await fetchEventsCombined(info.start, info.end); + let raw; + + // Используем новую систему множественных календарей, если доступна + if (window.calendarIntegration?.fetchEventsForVisibleCalendars) { + console.log('[CalendarWidget] Using calendar integration for event loading'); + raw = await window.calendarIntegration.fetchEventsForVisibleCalendars(info.start, info.end); + } else { + // Fallback на старую систему + console.log('[CalendarWidget] Using legacy fetchEventsCombined'); + raw = await fetchEventsCombined(info.start, info.end); + } + // Нормализуем id и цвет для FullCalendar const mapped = (Array.isArray(raw) ? raw : []).map((ev) => { const id = ev.id ?? ev.pk ?? ev.uuid ?? ev.slug ?? ev._id; diff --git a/backend/static/js/components/calendarWidgetIntegration.js b/backend/static/js/components/calendarWidgetIntegration.js new file mode 100644 index 00000000..3ef1e85a --- /dev/null +++ b/backend/static/js/components/calendarWidgetIntegration.js @@ -0,0 +1,247 @@ +/** + * @fileoverview Calendar Widget Integration - связывает calendarManager с calendarWidget + * @module components/calendarWidgetIntegration + */ + +import { initCalendarManager } from './calendarManager.js'; +import { initCalendarManageModal } from './calendarManageModal.js'; +import { getCalendarEvents } from '../api/calendarApi.js'; + +// Глобальное хранилище интеграции +let globalIntegration = null; + +/** + * Расширенная инициализация виджета календаря с поддержкой множественных календарей + * @param {Object} calendarWidgetInstance - Экземпляр calendarWidget + * @param {Object} options - Опции + * @returns {Object} Расширенный API + */ +export function integrateCalendarManager(calendarWidgetInstance, options = {}) { + if (!calendarWidgetInstance) { + console.warn('[CalendarIntegration] calendarWidget instance not provided'); + return null; + } + + let visibleCalendarIds = []; + let calendars = []; + + /** + * Загрузить события с учётом выбранных календарей + * @param {Date} start - Начало периода + * @param {Date} end - Конец периода + * @returns {Promise} События + */ + async function fetchEventsForVisibleCalendars(start, end) { + const startStr = formatDate(start); + const endStr = formatDate(end); + + console.log('[CalendarIntegration] Fetching events for visible calendars:', { + visibleCount: visibleCalendarIds.length, + totalCalendars: calendars.length, + range: `${startStr} - ${endStr}` + }); + + // Если нет календарей в новой системе, используем legacy режим + if (calendars.length === 0) { + console.log('[CalendarIntegration] Using legacy mode (no calendars configured)'); + return await getCalendarEvents({ + start: startStr, + end: endStr + }); + } + + // Если ни один календарь не выбран, возвращаем пустой массив + if (visibleCalendarIds.length === 0) { + console.log('[CalendarIntegration] No calendars selected, returning empty array'); + return []; + } + + // Загружаем события для каждого видимого календаря + const eventChunks = await Promise.all( + visibleCalendarIds.map(async (calendarId) => { + try { + const events = await getCalendarEvents({ + start: startStr, + end: endStr, + calendar_id: calendarId + }); + + console.log(`[CalendarIntegration] Loaded ${events?.length || 0} events for calendar ${calendarId}`); + + // Добавляем информацию о календаре к каждому событию + const calendar = calendars.find(c => c.id === calendarId); + return (events || []).map(event => ({ + ...event, + __calendar: calendar, + // Переопределяем цвет события цветом календаря + color: calendar?.color || event.color + })); + } catch (error) { + console.error(`[CalendarIntegration] Failed to load events for calendar ${calendarId}:`, error); + return []; + } + }) + ); + + // Объединяем все события + const allEvents = eventChunks.flat(); + console.log(`[CalendarIntegration] Total events loaded: ${allEvents.length}`); + + // Дедупликация по ID + const seen = new Set(); + const uniqueEvents = allEvents.filter(event => { + const id = event.id || event.pk; + if (!id || seen.has(id)) return false; + seen.add(id); + return true; + }); + + console.log(`[CalendarIntegration] Unique events after dedup: ${uniqueEvents.length}`); + return uniqueEvents; + } + + /** + * Форматировать дату в YYYY-MM-DD + * @param {Date} date + * @returns {string} + */ + function formatDate(date) { + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; + } + + /** + * Обработчик переключения видимости календаря + * @param {number} calendarId + * @param {boolean} isVisible + */ + function handleCalendarToggle(calendarId, isVisible) { + console.log(`[CalendarIntegration] Calendar ${calendarId} visibility: ${isVisible}`); + + // Обновляем список видимых календарей + if (isVisible) { + if (!visibleCalendarIds.includes(calendarId)) { + visibleCalendarIds.push(calendarId); + } + } else { + visibleCalendarIds = visibleCalendarIds.filter(id => id !== calendarId); + } + + // Перезагружаем события + if (calendarWidgetInstance.refetchEvents) { + calendarWidgetInstance.refetchEvents(); + } + } + + /** + * Обработчик изменения списка календарей + * @param {Array} newCalendars + * @param {Array} newVisibleIds + */ + function handleCalendarsChange(newCalendars, newVisibleIds) { + console.log(`[CalendarIntegration] Calendars updated:`, { + total: newCalendars.length, + visible: newVisibleIds.length + }); + + calendars = newCalendars; + visibleCalendarIds = newVisibleIds; + + // Перезагружаем события + if (calendarWidgetInstance.refetchEvents) { + calendarWidgetInstance.refetchEvents(); + } + } + + /** + * Обработчик успешного сохранения календаря + */ + function handleModalSuccess() { + console.log('[CalendarIntegration] Calendar saved, refreshing list'); + + // Обновляем список календарей + if (calendarManagerInstance) { + calendarManagerInstance.refresh(); + } + } + + // Инициализация менеджера календарей + const calendarManagerInstance = initCalendarManager({ + containerId: 'calendarListContainer', + onCalendarToggle: handleCalendarToggle, + onCalendarsChange: handleCalendarsChange + }); + + // Инициализация модального окна управления + const manageModalInstance = initCalendarManageModal({ + onSuccess: handleModalSuccess + }); + + // Привязываем кнопку создания календаря к модальному окну + const createCalendarBtn = document.querySelector('[data-bs-target="#calendarManageModal"]'); + if (createCalendarBtn && manageModalInstance) { + createCalendarBtn.addEventListener('click', () => { + manageModalInstance.openForCreate(); + }); + } + + // Создаём объект интеграции + const integration = { + /** + * Получить события для видимых календарей + */ + fetchEventsForVisibleCalendars, + + /** + * Получить ID видимых календарей + */ + getVisibleCalendarIds: () => [...visibleCalendarIds], + + /** + * Получить все календари + */ + getCalendars: () => [...calendars], + + /** + * Установить видимые календари + */ + setVisibleCalendars: (ids) => { + if (calendarManagerInstance) { + calendarManagerInstance.setVisibleCalendars(ids); + } + }, + + /** + * Обновить список календарей + */ + refresh: () => { + if (calendarManagerInstance) { + return calendarManagerInstance.refresh(); + } + }, + + /** + * Получить экземпляры компонентов + */ + instances: { + manager: calendarManagerInstance, + modal: manageModalInstance, + widget: calendarWidgetInstance + } + }; + + // Сохраняем глобально для доступа из других модулей + globalIntegration = integration; + + return integration; +} + +/** + * Получить глобальный экземпляр интеграции + * @returns {Object|null} + */ +export function getCalendarIntegration() { + return globalIntegration; +} diff --git a/backend/templates/includes/components/calendar_scripts.html b/backend/templates/includes/components/calendar_scripts.html index cde358ca..7354c681 100644 --- a/backend/templates/includes/components/calendar_scripts.html +++ b/backend/templates/includes/components/calendar_scripts.html @@ -3,9 +3,20 @@ + diff --git a/docs/in-progress/CALENDAR_OPTIONAL_ARCHITECTURE.md b/docs/in-progress/CALENDAR_OPTIONAL_ARCHITECTURE.md index f89ab3a3..665b31a0 100644 --- a/docs/in-progress/CALENDAR_OPTIONAL_ARCHITECTURE.md +++ b/docs/in-progress/CALENDAR_OPTIONAL_ARCHITECTURE.md @@ -66,12 +66,34 @@ DELETE /api/v1/calendar/subscriptions/{id}/ # Удалить подписк GET /api/v1/calendar/events/?calendar_id={id} # События календаря (legacy + new) ``` -### ⏳ Phase 3: Frontend (В планах) -- ⏳ UI для списка календарей с фильтрацией -- ⏳ Кнопки подписки/отписки -- ⏳ Модальное окно создания/редактирования календаря -- ⏳ Переключатель отображения календарей -- ⏳ Интеграция с FullCalendar.js +### ⏳ Phase 3: Frontend (В процессе) + +#### ✅ Part 1: UI Components (Завершено) +- ✅ `calendarsApi.js` - API wrapper для работы с календарями +- ✅ `calendarManager.js` - Компонент списка календарей +- ✅ `calendarManageModal.js` - Модальное окно создания/редактирования +- ✅ `calendar-manager.scss` - BEM-стили для компонентов +- ✅ `calendar_desktop.html` - Обновлённый UI с collapsible списком +- ✅ `calendar_modal_manage.html` - Шаблон модального окна +- ✅ Коммит: `feat(calendar): add frontend components for calendar management (Phase 3 - Part 1)` + +#### ✅ Part 2: Integration (Завершено) +- ✅ `calendarWidgetIntegration.js` - Интеграция менеджера с виджетом +- ✅ `calendarWidget.js` - Модифицирован для поддержки множественных календарей +- ✅ Фильтрация событий по выбранным календарям (`fetchEventsForVisibleCalendars`) +- ✅ Интеграция с FullCalendar.js через `window.calendarIntegration` +- ✅ Обновление цветов событий по календарям +- ✅ Синхронизация состояния видимости через callbacks +- ✅ Fallback на legacy режим для обратной совместимости +- ✅ Дедупликация событий по ID +- ✅ Логирование для debugging +- ✅ `calendar_scripts.html` обновлён для инициализации интеграции + +**Архитектурные решения:** +- Неинвазивная интеграция: `calendarWidget.js` проверяет наличие `window.calendarIntegration` +- Legacy режим автоматически активируется если новая система недоступна +- События загружаются с параметром `calendar_id` для каждого видимого календаря +- Цвет календаря переопределяет цвет события для визуальной группировки ### ⏳ Phase 4: Дополнительное тестирование (В планах) - ✅ Unit тесты для моделей (через API тесты) diff --git a/docs/in-progress/PHASE3_PART2_INTEGRATION.md b/docs/in-progress/PHASE3_PART2_INTEGRATION.md new file mode 100644 index 00000000..7b14d659 --- /dev/null +++ b/docs/in-progress/PHASE3_PART2_INTEGRATION.md @@ -0,0 +1,238 @@ +# Phase 3 Part 2: Calendar Widget Integration + +**Дата:** 11 февраля 2026 г. +**Статус:** ✅ Завершено +**Цель:** Интеграция компонентов управления календарями с существующим виджетом FullCalendar + +--- + +## 🎯 Задачи + +### ✅ Создание слоя интеграции +- **Файл:** `backend/static/js/components/calendarWidgetIntegration.js` +- **Назначение:** Связать `calendarManager` с `calendarWidget` +- **Экспорт:** + - `integrateCalendarManager(calendarWidgetInstance, options)` - главная функция + - `getCalendarIntegration()` - доступ к глобальному экземпляру + +### ✅ Модификация calendarWidget.js +- **Файл:** `backend/static/js/components/calendarWidget.js` +- **Изменения:** Обновлён callback `events` в конфигурации FullCalendar (строка ~965) +- **Логика:** + ```javascript + // Проверка наличия новой системы + if (window.calendarIntegration?.fetchEventsForVisibleCalendars) { + // Используем фильтрацию по выбранным календарям + raw = await window.calendarIntegration.fetchEventsForVisibleCalendars(info.start, info.end); + } else { + // Fallback на legacy систему + raw = await fetchEventsCombined(info.start, info.end); + } + ``` + +### ✅ Обновление инициализации +- **Файл:** `backend/templates/includes/components/calendar_scripts.html` +- **Изменения:** + - Добавлен импорт `calendarWidgetIntegration.js` + - Вызов `integrateCalendarManager(widget)` после инициализации виджета + - Экспорт `window.calendarIntegration` для глобального доступа + +--- + +## 🔧 Архитектурные решения + +### 1. Неинвазивная интеграция +- `calendarWidget.js` не имеет жёстких зависимостей на новую систему +- Проверка `window.calendarIntegration` перед использованием +- Автоматический fallback на legacy режим + +### 2. Состояние приложения +```javascript +// Внутреннее состояние integrateCalendarManager() +let visibleCalendarIds = []; // Массив ID видимых календарей +let calendars = []; // Массив объектов календарей +``` + +### 3. Callbacks для синхронизации +```javascript +// При переключении видимости календаря +onCalendarToggle: (calendarId, isVisible) => { + // Обновляем visibleCalendarIds + // Вызываем calendarWidget.refetchEvents() +} + +// При изменении списка календарей (CRUD) +onCalendarsChange: (newCalendars, newVisibleIds) => { + // Обновляем локальное состояние + // Вызываем calendarWidget.refetchEvents() +} + +// При сохранении календаря в модальном окне +onSuccess: () => { + // Обновляем список через calendarManager.refresh() +} +``` + +### 4. Загрузка событий + +#### Новый режим (множественные календари): +```javascript +async function fetchEventsForVisibleCalendars(start, end) { + // Если нет календарей - используем legacy + if (calendars.length === 0) { + return await getCalendarEvents({ start, end }); + } + + // Если ни один не выбран - пустой массив + if (visibleCalendarIds.length === 0) { + return []; + } + + // Загружаем события для каждого видимого календаря + const eventChunks = await Promise.all( + visibleCalendarIds.map(calendarId => + getCalendarEvents({ start, end, calendar_id: calendarId }) + ) + ); + + // Объединяем и дедуплицируем + const allEvents = eventChunks.flat(); + const uniqueEvents = deduplicateById(allEvents); + + return uniqueEvents; +} +``` + +#### Legacy режим: +```javascript +// Старая логика без изменений +const events = await fetchEventsCombined(start, end); +``` + +### 5. Обработка цветов +```javascript +// Цвет календаря переопределяет цвет события +return (events || []).map(event => ({ + ...event, + __calendar: calendar, + color: calendar?.color || event.color // Приоритет у календаря +})); +``` + +--- + +## 📦 Public API + +### window.calendarIntegration + +```javascript +{ + // Загрузить события для видимых календарей + fetchEventsForVisibleCalendars: (start, end) => Promise, + + // Получить ID видимых календарей + getVisibleCalendarIds: () => number[], + + // Получить все календари + getCalendars: () => Calendar[], + + // Установить видимые календари + setVisibleCalendars: (ids: number[]) => void, + + // Обновить список календарей + refresh: () => Promise, + + // Доступ к экземплярам компонентов + instances: { + manager: CalendarManager, + modal: CalendarManageModal, + widget: CalendarWidget + } +} +``` + +--- + +## 🔍 Логирование + +Все ключевые операции логируются в консоль с префиксом `[CalendarIntegration]` или `[CalendarWidget]`: + +```javascript +[CalendarIntegration] Fetching events for visible calendars: { visibleCount: 2, totalCalendars: 5, range: "2026-02-01 - 2026-02-28" } +[CalendarIntegration] Loaded 15 events for calendar 1 +[CalendarIntegration] Loaded 8 events for calendar 2 +[CalendarIntegration] Total events loaded: 23 +[CalendarIntegration] Unique events after dedup: 22 +[CalendarWidget] Using calendar integration for event loading +``` + +--- + +## ✅ Результаты + +### Файлы изменены: +1. **Создан:** `backend/static/js/components/calendarWidgetIntegration.js` (~250 lines) +2. **Модифицирован:** `backend/static/js/components/calendarWidget.js` (events callback) +3. **Обновлён:** `backend/templates/includes/components/calendar_scripts.html` (инициализация) + +### Функциональность: +- ✅ Неинвазивная интеграция с обратной совместимостью +- ✅ Фильтрация событий по выбранным календарям +- ✅ Автоматическое обновление при переключении видимости +- ✅ Дедупликация событий по ID +- ✅ Переопределение цветов событий цветами календарей +- ✅ Подробное логирование для отладки +- ✅ Fallback на legacy режим + +### Обратная совместимость: +- ✅ Если новая система недоступна → используется старая логика +- ✅ Если нет настроенных календарей → используется старая логика +- ✅ Существующий код не сломан + +--- + +## 🚀 Следующие шаги + +### Phase 3 - Готово к коммиту +```bash +git add backend/static/js/components/calendarWidgetIntegration.js +git add backend/static/js/components/calendarWidget.js +git add backend/templates/includes/components/calendar_scripts.html +git add docs/in-progress/CALENDAR_OPTIONAL_ARCHITECTURE.md +git add docs/in-progress/PHASE3_PART2_INTEGRATION.md +git commit -m "feat(calendar): integrate calendar manager with widget (Phase 3 - Part 2)" +``` + +### Phase 4: Integration Testing +- ⏳ Запустить сервер и протестировать UI +- ⏳ Создать несколько календарей разных типов +- ⏳ Проверить переключение видимости +- ⏳ Проверить создание/редактирование событий +- ⏳ Проверить подписку на календари +- ⏳ Проверить права доступа + +### Phase 5: Documentation +- ⏳ Обновить README с новым функционалом +- ⏳ Создать user guide +- ⏳ Задокументировать API endpoints + +--- + +## 📝 Примечания + +### Технические детали: +1. **Глобальное состояние:** `window.calendarIntegration` доступен глобально для отладки и расширений +2. **Производительность:** События загружаются параллельно через `Promise.all()` +3. **Кеширование:** Используется существующий механизм кеширования из `dataManager.js` +4. **Безопасность:** Права доступа проверяются на backend через API + +### Дизайн-решения: +1. **Callback-based integration:** Позволяет компонентам оставаться независимыми +2. **Optional chaining:** `window.calendarIntegration?.fetchEventsForVisibleCalendars` для безопасной проверки +3. **Defensive programming:** Проверки на `null`/`undefined` во всех ключевых местах +4. **Logging:** Подробное логирование помогает в debugging + +--- + +**Автор:** GitHub Copilot +**Дата завершения:** 11 февраля 2026 г.