/** * @fileoverview Calendar Manager Component - управление списком календарей * @module components/calendarManager */ import { getMyCalendars, invalidateCalendarsCache, invalidateCalendarEventsCache, } from "../api/calendarsApi.js"; import { CALENDAR_TYPES, CALENDAR_COLORS, createLegacyDeptId, } from "../constants/calendarTypes.js"; import { initCalendarEventDragDrop } from "./calendarEventDragDrop.js"; /** * Инициализация менеджера календарей * @param {Object} options - Параметры * @param {string} options.containerId - ID контейнера для списка календарей * @param {Function} options.onCalendarToggle - Callback при переключении видимости календаря * @param {Function} options.onCalendarsChange - Callback при изменении списка календарей * @returns {Object} API менеджера */ export function initCalendarManager(options = {}) { const { containerId = "calendarListContainer", onCalendarToggle = () => {}, onCalendarsChange = () => {}, } = options; const container = document.getElementById(containerId); if (!container) { console.warn(`[CalendarManager] Container #${containerId} not found`); return null; } // Состояние let calendars = []; let visibleCalendarIds = new Set(); let eventCounts = new Map(); // Счётчики событий для папок: folderId -> count let dragDropInstance = null; /** * Создать псевдо-календари для legacy режима * @returns {Array} Legacy календари */ function createLegacyCalendars() { const legacyCalendars = [ { id: CALENDAR_TYPES.LEGACY_COMPANY, title: "Компания", description: "Корпоративные события", color: CALENDAR_COLORS.COMPANY, calendar_type: CALENDAR_TYPES.COMPANY, is_legacy: true, is_global: true, user_can_edit: false, user_can_view: true, is_subscribed: true, }, { id: CALENDAR_TYPES.LEGACY_PERSONAL, title: "Личный календарь", description: "Мои личные события", color: CALENDAR_COLORS.PERSONAL, calendar_type: CALENDAR_TYPES.PERSONAL, is_legacy: true, is_personal: true, user_can_edit: true, user_can_view: true, is_subscribed: true, }, ]; // Добавить календари отделов из calendarWidget const deptObjects = window.calendarWidget?.getDepartments?.() || []; console.log("[CalendarManager] Department objects:", deptObjects); deptObjects.forEach((dept) => { const deptId = dept?.id || dept?.pk || dept?.department_id; if (!deptId) { console.warn("[CalendarManager] Department object missing ID:", dept); return; } legacyCalendars.push({ id: createLegacyDeptId(deptId), title: dept.name || `Отдел ${deptId}`, description: "События отдела", color: CALENDAR_COLORS.DEPARTMENT, calendar_type: CALENDAR_TYPES.DEPARTMENT, department_id: parseInt(deptId, 10), is_legacy: true, is_department: true, user_can_edit: false, user_can_view: true, is_subscribed: true, }); }); console.log("[CalendarManager] Created legacy calendars:", legacyCalendars); return legacyCalendars; } /** * Загрузить список календарей */ async function loadCalendars() { try { // Загрузить календари из django-scheduler API const newCalendars = await getMyCalendars(); // Добавить legacy календари const legacyCalendars = createLegacyCalendars(); // Объединить оба списка calendars = [...legacyCalendars, ...newCalendars]; console.log("[CalendarManager] Loaded calendars:", { legacy: legacyCalendars.length, new: newCalendars.length, total: calendars.length, calendarIds: calendars.map((c) => ({ id: c.id, title: c.title })), }); // Детальная информация о новых календарях console.log( "[CalendarManager] New calendars details:", newCalendars.map((c) => ({ id: c.id, title: c.title, name: c.name, slug: c.slug, })), ); // Загрузить видимость календарей из localStorage visibleCalendarIds = new Set(); calendars.forEach((cal) => { // Используем localStorage для всех календарей const storageKey = `calendar_visible_${cal.id}`; const stored = localStorage.getItem(storageKey); const isVisible = stored === null ? true : stored === "true"; if (isVisible) { visibleCalendarIds.add(cal.id); } }); console.log( "[CalendarManager] Visible calendar IDs:", Array.from(visibleCalendarIds), ); // Загружаем счётчики событий для папок await loadEventCounts(); render(); onCalendarsChange(calendars, Array.from(visibleCalendarIds)); } catch (error) { console.error("[CalendarManager] Failed to load calendars:", error); container.innerHTML = `
Не удалось загрузить календари
`; } } /** * Загрузить счётчики событий для папок и календарей */ async function loadEventCounts() { try { const { getEvents } = await import("../api/calendarsApi.js"); // Диапазон для загрузки событий (например, 1 год назад - 1 год вперед) const now = new Date(); const start = new Date(now.getFullYear() - 1, 0, 1); // 1 янв прошлого года const end = new Date(now.getFullYear() + 1, 11, 31); // 31 дек следующего года const startStr = start.toISOString().split("T")[0]; // YYYY-MM-DD const endStr = end.toISOString().split("T")[0]; // Для каждой legacy папки загружаем количество событий const legacyFolders = calendars.filter((cal) => String(cal.id).startsWith("legacy-"), ); for (const folder of legacyFolders) { let eventsParams = { start: startStr, end: endStr }; if (folder.id === "legacy-company") { // Для компании: не передаем ничего, бэкенд по умолчанию вернет company события // eventsParams уже содержит start/end } else if (folder.id === "legacy-personal") { // Для личного: передаем ID текущего пользователя const currentUserId = window.currentUserId || document.body.dataset.userId; if (currentUserId) { eventsParams.employee_id = parseInt(currentUserId, 10); } } else if (folder.id.startsWith("legacy-dept-")) { const deptId = parseInt(folder.id.replace("legacy-dept-", ""), 10); eventsParams.department_id = deptId; } try { const events = await getEvents(eventsParams); eventCounts.set(folder.id, events.length || 0); } catch (err) { console.warn( `[CalendarManager] Failed to load event count for ${folder.id}:`, err, ); eventCounts.set(folder.id, 0); } } // Для настраиваемых календарей загружаем количество событий const customCalendars = calendars.filter( (cal) => !String(cal.id).startsWith("legacy-"), ); for (const calendar of customCalendars) { try { const events = await getEvents({ calendar_id: calendar.id, start: startStr, end: endStr, }); eventCounts.set(calendar.id, events.length || 0); } catch (err) { console.warn( `[CalendarManager] Failed to load event count for calendar ${calendar.id}:`, err, ); eventCounts.set(calendar.id, 0); } } console.log( "[CalendarManager] Event counts loaded:", Object.fromEntries(eventCounts), ); } catch (error) { console.error("[CalendarManager] Failed to load event counts:", error); } } /** * Переключить видимость календаря * @param {string|number} calendarId */ async function toggleCalendarVisibility(calendarId) { const isVisible = !visibleCalendarIds.has(calendarId); // Обновить локальное состояние if (isVisible) { visibleCalendarIds.add(calendarId); } else { visibleCalendarIds.delete(calendarId); } // Сохранить в localStorage (django-scheduler не имеет subscriptions) try { const storageKey = `calendar_visible_${calendarId}`; localStorage.setItem(storageKey, String(isVisible)); console.log( `[CalendarManager] Saved calendar visibility to localStorage: ${calendarId} = ${isVisible}`, ); } catch (error) { console.error("[CalendarManager] Failed to save visibility:", error); // Откатить изменение при ошибке if (isVisible) { visibleCalendarIds.delete(calendarId); } else { visibleCalendarIds.add(calendarId); } } render(); onCalendarToggle(calendarId, visibleCalendarIds.has(calendarId)); } /** * Подписаться на календарь (заглушка для совместимости) * @deprecated django-scheduler не использует subscriptions * @param {number} calendarId */ async function subscribe(calendarId) { console.warn( "[CalendarManager] subscribe() deprecated - django-scheduler doesn't use subscriptions", ); const calendar = calendars.find((c) => c.id === calendarId); if (calendar) { // Просто показываем календарь if (!visibleCalendarIds.has(calendarId)) { await toggleCalendarVisibility(calendarId); } } } /** * Отписаться от календаря (заглушка для совместимости) * @deprecated django-scheduler не использует subscriptions * @param {number} calendarId */ async function unsubscribe(calendarId) { console.warn( "[CalendarManager] unsubscribe() deprecated - django-scheduler doesn't use subscriptions", ); const calendar = calendars.find((c) => c.id === calendarId); if (calendar) { // Просто скрываем календарь if (visibleCalendarIds.has(calendarId)) { await toggleCalendarVisibility(calendarId); } } } /** * Преобразовать legacy календарь в настраиваемый Calendar * @param {string} legacyId - например "legacy-company", "legacy-personal", "legacy-dept-4" */ async function convertLegacyToCalendar(legacyId) { // Находим legacy календарь const legacyCalendar = calendars.find((cal) => cal.id === legacyId); if (!legacyCalendar) { alert("Календарь не найден"); return; } const confirmMessage = `Преобразовать "${legacyCalendar.title}" в настраиваемый календарь?\n\nВсе события будут перенесены в новый календарь.`; if (!confirm(confirmMessage)) return; try { // Определяем параметры нового календаря на основе legacy ID let calendarData = { title: legacyCalendar.title, description: `Преобразован из системного календаря`, color: legacyCalendar.color, visibility: "public", }; if (legacyId === "legacy-company") { calendarData.visibility = "public"; } else if (legacyId === "legacy-personal") { calendarData.visibility = "private"; } else if (legacyId.startsWith("legacy-dept-")) { const deptId = legacyId.replace("legacy-dept-", ""); calendarData.owner_department = parseInt(deptId, 10); calendarData.visibility = "department"; } console.log("[CalendarManager] Creating new calendar:", calendarData); // Создаём новый Calendar const { createCalendar } = await import("../api/calendarsApi.js"); const newCalendar = await createCalendar(calendarData); console.log("[CalendarManager] New calendar created:", newCalendar); // Получаем все события из legacy календаря const { getEvents } = await import("../api/calendarsApi.js"); let eventsParams = {}; if (legacyId === "legacy-personal") { eventsParams.employee_id = "me"; // Личные события } else if (legacyId.startsWith("legacy-dept-")) { const deptId = legacyId.replace("legacy-dept-", ""); eventsParams.department_id = parseInt(deptId, 10); } else if (legacyId === "legacy-company") { // Компания - события без department и employee eventsParams.scope = "company"; } console.log( "[CalendarManager] Fetching events with params:", eventsParams, ); const events = await getEvents(eventsParams); console.log(`[CalendarManager] Found ${events.length} events to migrate`); if (events.length === 0) { alert( `Календарь "${legacyCalendar.title}" преобразован.\n\nСобытий для переноса не найдено.`, ); await loadCalendars(); return; } // Переносим события (обновляем поле calendar) const { updateEvent } = await import("../api/calendarsApi.js"); let migratedCount = 0; let failedCount = 0; for (const event of events) { try { await updateEvent(event.id, { calendar: newCalendar.id }); migratedCount++; } catch (err) { console.error( `[CalendarManager] Failed to migrate event ${event.id}:`, err, ); failedCount++; } } console.log( `[CalendarManager] Migration complete: ${migratedCount} success, ${failedCount} failed`, ); alert( `Календарь "${legacyCalendar.title}" преобразован!\n\nПеренесено событий: ${migratedCount}\nОшибок: ${failedCount}`, ); // Перезагружаем список календарей и события await loadCalendars(); if (typeof window.calendarWidget?.refetchEvents === "function") { window.calendarWidget.refetchEvents(); } } catch (error) { console.error("[CalendarManager] Conversion failed:", error); alert(`Ошибка преобразования:\n${error.message || error}`); } } /** * Создать календарь внутри папки * @param {string} folderId - ID папки (legacy календаря) */ async function createCalendarInFolder(folderId) { // Определяем параметры на основе папки let calendarData = { title: "", description: "", color: "#0d6efd", visibility: "public", }; if (folderId === "legacy-company") { // Глобальный календарь: НЕ указываем owner_user и owner_department calendarData.visibility = "public"; calendarData.title = prompt( "Название календаря (глобальный):", "Новый календарь", ); } else if (folderId === "legacy-personal") { // Личный календарь: указываем owner_user = текущий пользователь const currentUserId = window.currentUserId || document.body.dataset.userId; if (!currentUserId) { alert("Не удалось определить текущего пользователя"); return; } calendarData.owner_user = parseInt(currentUserId, 10); calendarData.visibility = "private"; calendarData.title = prompt( "Название личного календаря:", "Мой календарь", ); } else if (folderId.startsWith("legacy-dept-")) { // Календарь отдела: указываем owner_department const deptId = parseInt(folderId.replace("legacy-dept-", ""), 10); calendarData.owner_department = deptId; calendarData.visibility = "department"; calendarData.title = prompt( "Название календаря отдела:", "Календарь отдела", ); console.log("[CalendarManager] Creating department calendar:", { owner_department: deptId, visibility: "department", title: calendarData.title, }); } if (!calendarData.title) { return; // Пользователь отменил } try { const { createCalendar } = await import("../api/calendarsApi.js"); console.log( "[CalendarManager] Creating calendar with data:", calendarData, ); const newCalendar = await createCalendar(calendarData); console.log("[CalendarManager] Calendar created in folder:", newCalendar); alert(`Календарь "${newCalendar.title}" создан!`); await loadCalendars(); } catch (error) { console.error("[CalendarManager] Failed to create calendar:", error); alert(`Ошибка создания календаря:\n${error.message || error}`); } } /** * Получить иконку для типа календаря * @param {Object} calendar * @returns {string} HTML иконки */ function getCalendarIcon(calendar) { if (calendar.is_personal) { return ''; } else if (calendar.is_department) { return ''; } else if (calendar.is_global) { return ''; } return ''; } /** * Получить бейдж владельца * @param {Object} calendar * @returns {string} HTML бейджа */ function getOwnerBadge(calendar) { if (calendar.is_personal && calendar.owner_user_name) { return `${calendar.owner_user_name}`; } else if (calendar.is_department && calendar.owner_department_name) { return `${calendar.owner_department_name}`; } else if (calendar.is_global) { return `Общий`; } return ""; } /** * Рендер списка календарей */ function render() { if (calendars.length === 0) { container.innerHTML = `
Нет доступных календарей
`; return; } // Разделяем календари на legacy (папки) и настраиваемые (календари) const legacyFolders = calendars.filter((cal) => String(cal.id).startsWith("legacy-"), ); const customCalendars = calendars.filter( (cal) => !String(cal.id).startsWith("legacy-"), ); /** * Генерация HTML для папки (legacy календарь) */ const renderFolder = (folder) => { const isVisible = visibleCalendarIds.has(folder.id); const folderId = folder.id; // Определяем дочерние календари для этой папки let childCalendars = []; if (folderId === "legacy-company") { // Под папкой "Компания" - глобальные календари (без owner) childCalendars = customCalendars.filter( (cal) => cal.is_global && !cal.owner_user_id && !cal.owner_department_id, ); console.log( `[CalendarManager] Folder "${folderId}": ${childCalendars.length} global calendars`, customCalendars.map((c) => ({ id: c.id, title: c.title, is_global: c.is_global, owner_user_id: c.owner_user_id, owner_department_id: c.owner_department_id, })), ); } else if (folderId === "legacy-personal") { // Под папкой "Личный" - личные календари текущего пользователя childCalendars = customCalendars.filter((cal) => cal.is_personal); console.log( `[CalendarManager] Folder "${folderId}": ${childCalendars.length} personal calendars`, customCalendars.map((c) => ({ id: c.id, title: c.title, is_personal: c.is_personal, owner_user_id: c.owner_user_id, })), ); } else if (folderId.startsWith("legacy-dept-")) { // Под папкой отдела - календари этого отдела const deptId = parseInt(folderId.replace("legacy-dept-", ""), 10); console.log( `[CalendarManager] Filtering for dept ${deptId}:`, customCalendars.map((c) => ({ id: c.id, title: c.title, is_department: c.is_department, owner_department_id: c.owner_department_id, matches: c.owner_department_id === deptId, comparison: `${c.owner_department_id} === ${deptId}`, })), ); childCalendars = customCalendars.filter( (cal) => cal.owner_department_id === deptId, ); console.log( `[CalendarManager] Folder "${folderId}": ${childCalendars.length} department calendars for dept ${deptId}`, customCalendars.map((c) => ({ id: c.id, title: c.title, is_department: c.is_department, owner_department_id: c.owner_department_id, })), ); } const childrenHtml = childCalendars .map((cal) => renderChildCalendar(cal)) .join(""); // Получаем количество событий из кеша const eventCount = eventCounts.get(folderId) || 0; return `
${ childrenHtml ? `
${childrenHtml}
` : `
Нет календарей в папке
` }
`; }; /** * Генерация HTML для календаря внутри папки */ const renderChildCalendar = (calendar) => { const isVisible = visibleCalendarIds.has(calendar.id); const canEdit = calendar.user_can_edit; const isSubscribed = calendar.is_subscribed; const eventCount = eventCounts.get(calendar.id) || 0; return `
${ canEdit ? ` ` : "" } ${ !isSubscribed ? ` ` : ` ` }
`; }; // Собираем HTML: каждая legacy папка + её дочерние календари const html = legacyFolders.map(renderFolder).join(""); container.innerHTML = html; attachEventListeners(); } /** * Прикрепить обработчики событий */ function attachEventListeners() { // Переключение видимости container .querySelectorAll(".calendar-visibility-toggle") .forEach((checkbox) => { checkbox.addEventListener("change", (e) => { // НЕ парсим ID - он может быть строкой (legacy) или числом (новые календари) const calendarId = e.target.dataset.calendarId; // Конвертируем в число ТОЛЬКО если это число const id = /^\d+$/.test(calendarId) ? parseInt(calendarId, 10) : calendarId; toggleCalendarVisibility(id); }); }); // Создание календаря в папке container.querySelectorAll(".calendar-create-child-btn").forEach((btn) => { btn.addEventListener("click", (e) => { const folderId = e.currentTarget.dataset.folderId; createCalendarInFolder(folderId); }); }); // Преобразование legacy календаря в настраиваемый container.querySelectorAll(".calendar-convert-btn").forEach((btn) => { btn.addEventListener("click", (e) => { const calendarId = e.currentTarget.dataset.calendarId; convertLegacyToCalendar(calendarId); }); }); // Подписка (только для новых календарей с числовым ID) container.querySelectorAll(".calendar-subscribe-btn").forEach((btn) => { btn.addEventListener("click", (e) => { const calendarId = parseInt(e.currentTarget.dataset.calendarId, 10); if (!isNaN(calendarId)) { subscribe(calendarId); } }); }); // Отписка (только для новых календарей с числовым ID) container.querySelectorAll(".calendar-unsubscribe-btn").forEach((btn) => { btn.addEventListener("click", (e) => { const calendarId = parseInt(e.currentTarget.dataset.calendarId, 10); if (!isNaN(calendarId)) { unsubscribe(calendarId); } }); }); // Редактирование (делегируем наверх) container.querySelectorAll(".calendar-edit-btn").forEach((btn) => { btn.addEventListener("click", (e) => { const calendarId = parseInt(e.currentTarget.dataset.calendarId, 10); const calendar = calendars.find((c) => c.id === calendarId); if (calendar) { // Dispatch custom event для открытия модального окна document.dispatchEvent( new CustomEvent("calendar:edit", { detail: calendar }), ); } }); }); } /** * Обновить список календарей */ async function refresh() { invalidateCalendarsCache(); invalidateCalendarEventsCache(); // Инвалидируем кеш событий после перемещения await loadCalendars(); } /** * Получить ID видимых календарей * @returns {Array} */ function getVisibleCalendarIds() { return Array.from(visibleCalendarIds); } /** * Установить видимость календарей * @param {Array} calendarIds */ function setVisibleCalendars(calendarIds) { visibleCalendarIds = new Set(calendarIds); render(); onCalendarsChange(calendars, Array.from(visibleCalendarIds)); } // Инициализация loadCalendars(); // Инициализация drag & drop dragDropInstance = initCalendarEventDragDrop({ onEventMoved: () => { refresh(); onCalendarsChange(calendars, Array.from(visibleCalendarIds)); }, }); // Public API return { refresh, getVisibleCalendarIds, setVisibleCalendars, loadCalendars, }; }