From 826298ff0896e61f7a966a153c1105587729be8d 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 09:42:18 +0300 Subject: [PATCH] feat(calendar): add frontend components for calendar management (Phase 3 - Part 1) Added API Integration: - calendarsApi.js: Complete API wrapper for /api/v1/calendar/calendars/ * CRUD operations (create, read, update, delete) * Subscribe/unsubscribe endpoints * My subscriptions endpoint * Cache invalidation support Added UI Components: - calendarManager.js: Calendar list management component * Displays available calendars with color indicators * Toggle calendar visibility (checkboxes) * Subscribe/unsubscribe buttons * Edit calendar button (for owners) * Real-time calendar filtering - calendarManageModal.js: Calendar creation/editing modal * Create personal/public/department calendars * Edit calendar properties (title, description, color) * Delete calendar (owner only) * Department selection for department calendars * Auto-subscription settings Added Styles: - calendar-manager.scss: BEM-based styling * Calendar list with glassmorphism design * Color indicators and icons * Hover states and animations * Responsive design for mobile * Empty state styling Updated Templates: - calendar_desktop.html: Added calendar list UI * Replaced dropdown with collapsible calendar list * Added manage calendar button * Updated create event button icon - calendar_modal_manage.html: New modal template * Form fields for calendar properties * Radio buttons for calendar types * Department selector * Auto-subscription checkboxes * Delete button for owners - rightbar_calendar.html: Included new modal Updated Imports: - main.scss: Added calendar-manager component import Status: Phase 3 Part 1 complete (UI components ready) Next: Part 2 - Integration with calendarWidget.js for event filtering --- backend/static/js/api/calendarsApi.js | 312 ++++++++++++++++++ .../js/components/calendarManageModal.js | 272 +++++++++++++++ .../static/js/components/calendarManager.js | 289 ++++++++++++++++ .../scss/components/calendar-manager.scss | 186 +++++++++++ backend/static/scss/main.scss | 1 + .../includes/components/calendar_desktop.html | 65 +++- .../components/calendar_modal_manage.html | 176 ++++++++++ .../templates/includes/rightbar_calendar.html | 1 + 8 files changed, 1284 insertions(+), 18 deletions(-) create mode 100644 backend/static/js/api/calendarsApi.js create mode 100644 backend/static/js/components/calendarManageModal.js create mode 100644 backend/static/js/components/calendarManager.js create mode 100644 backend/static/scss/components/calendar-manager.scss create mode 100644 backend/templates/includes/components/calendar_modal_manage.html diff --git a/backend/static/js/api/calendarsApi.js b/backend/static/js/api/calendarsApi.js new file mode 100644 index 00000000..241cd806 --- /dev/null +++ b/backend/static/js/api/calendarsApi.js @@ -0,0 +1,312 @@ +/** + * @fileoverview Calendars API - работа с календарями (CRUD + подписки) + * @module api/calendarsApi + */ + +import { dataManager } from '../managers/dataManager.js'; + +/** + * Получить токен авторизации + * @returns {string} + * @private + */ +function getAccessToken() { + const meta = document.querySelector('meta[name="api-access"]'); + const token = meta?.getAttribute('content')?.trim(); + if (token) return token; + + try { + return localStorage.getItem('api.access') || ''; + } catch { + return ''; + } +} + +/** + * Получить CSRF токен + * @returns {string} + * @private + */ +function getCsrfToken() { + return document.querySelector('[name=csrfmiddlewaretoken]')?.value || + document.querySelector('meta[name="csrf-token"]')?.content || + getCookie('csrftoken') || ''; +} + +/** + * Получить cookie по имени + * @param {string} name + * @returns {string} + * @private + */ +function getCookie(name) { + const value = `; ${document.cookie}`; + const parts = value.split(`; ${name}=`); + if (parts.length === 2) return parts.pop().split(';').shift(); + return ''; +} + +/** + * Создать заголовки с авторизацией + * @param {boolean} [includeContentType=true] - Добавить Content-Type + * @returns {Object} + * @private + */ +function authHeaders(includeContentType = true) { + const token = getAccessToken(); + const headers = { + 'Accept': 'application/json', + ...(token ? { 'Authorization': `Bearer ${token}` } : {}), + 'X-CSRFToken': getCsrfToken() + }; + + if (includeContentType) { + headers['Content-Type'] = 'application/json'; + } + + return headers; +} + +/** + * Получить список доступных календарей текущего пользователя + * @param {number} [ttl=60000] - Time to live кеша в мс + * @returns {Promise} Массив календарей + */ +export async function getMyCalendars(ttl = 60000) { + const key = 'calendars:my'; + + return dataManager.fetch( + key, + async () => { + const url = new URL('/api/v1/calendar/calendars/', window.location.origin); + + const response = await fetch(url.toString(), { + headers: authHeaders(false) + }); + + if (response.status === 401) { + console.warn('[CalendarsAPI] 401 Unauthorized'); + return []; + } + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + return data.results || []; + }, + ttl + ); +} + +/** + * Получить календарь по ID + * @param {number} calendarId - ID календаря + * @param {number} [ttl=60000] - Time to live кеша в мс + * @returns {Promise} Календарь + */ +export async function getCalendar(calendarId, ttl = 60000) { + const key = `calendars:${calendarId}`; + + return dataManager.fetch( + key, + async () => { + const url = new URL(`/api/v1/calendar/calendars/${calendarId}/`, window.location.origin); + + const response = await fetch(url.toString(), { + headers: authHeaders(false) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + return response.json(); + }, + ttl + ); +} + +/** + * Создать новый календарь + * @param {Object} calendarData - Данные календаря + * @param {string} calendarData.title - Название + * @param {string} [calendarData.description] - Описание + * @param {string} [calendarData.color] - Цвет (#RRGGBB) + * @param {string} [calendarData.icon] - Иконка + * @param {string} [calendarData.visibility] - Видимость (public/private/department/custom) + * @param {number} [calendarData.owner_user] - ID владельца-пользователя + * @param {number} [calendarData.owner_department] - ID владельца-отдела + * @returns {Promise} Созданный календарь + */ +export async function createCalendar(calendarData) { + const url = new URL('/api/v1/calendar/calendars/', window.location.origin); + + const response = await fetch(url.toString(), { + method: 'POST', + headers: authHeaders(), + body: JSON.stringify(calendarData) + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: response.statusText })); + throw new Error(error.detail || `HTTP ${response.status}`); + } + + // Инвалидируем кеш списка календарей + dataManager.invalidate('calendars:my'); + + return response.json(); +} + +/** + * Обновить календарь + * @param {number} calendarId - ID календаря + * @param {Object} updates - Обновления + * @returns {Promise} Обновленный календарь + */ +export async function updateCalendar(calendarId, updates) { + const url = new URL(`/api/v1/calendar/calendars/${calendarId}/`, window.location.origin); + + const response = await fetch(url.toString(), { + method: 'PATCH', + headers: authHeaders(), + body: JSON.stringify(updates) + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: response.statusText })); + throw new Error(error.detail || `HTTP ${response.status}`); + } + + // Инвалидируем кеш + dataManager.invalidate(`calendars:${calendarId}`); + dataManager.invalidate('calendars:my'); + + return response.json(); +} + +/** + * Удалить календарь + * @param {number} calendarId - ID календаря + * @returns {Promise} + */ +export async function deleteCalendar(calendarId) { + const url = new URL(`/api/v1/calendar/calendars/${calendarId}/`, window.location.origin); + + const response = await fetch(url.toString(), { + method: 'DELETE', + headers: authHeaders(false) + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: response.statusText })); + throw new Error(error.detail || `HTTP ${response.status}`); + } + + // Инвалидируем кеш + dataManager.invalidate(`calendars:${calendarId}`); + dataManager.invalidate('calendars:my'); +} + +/** + * Подписаться на календарь + * @param {number} calendarId - ID календаря + * @param {Object} [options] - Опции подписки + * @param {boolean} [options.can_edit] - Право редактирования + * @param {boolean} [options.can_manage] - Право управления + * @returns {Promise} Данные подписки + */ +export async function subscribeToCalendar(calendarId, options = {}) { + const url = new URL( + `/api/v1/calendar/calendars/${calendarId}/subscribe/`, + window.location.origin + ); + + const response = await fetch(url.toString(), { + method: 'POST', + headers: authHeaders(), + body: JSON.stringify(options) + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: response.statusText })); + throw new Error(error.detail || `HTTP ${response.status}`); + } + + // Инвалидируем кеш + dataManager.invalidate(`calendars:${calendarId}`); + dataManager.invalidate('calendars:my'); + dataManager.invalidate('subscriptions:my'); + + return response.json(); +} + +/** + * Отписаться от календаря + * @param {number} calendarId - ID календаря + * @returns {Promise} + */ +export async function unsubscribeFromCalendar(calendarId) { + const url = new URL( + `/api/v1/calendar/calendars/${calendarId}/unsubscribe/`, + window.location.origin + ); + + const response = await fetch(url.toString(), { + method: 'POST', + headers: authHeaders(false) + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: response.statusText })); + throw new Error(error.detail || `HTTP ${response.status}`); + } + + // Инвалидируем кеш + dataManager.invalidate(`calendars:${calendarId}`); + dataManager.invalidate('calendars:my'); + dataManager.invalidate('subscriptions:my'); +} + +/** + * Получить мои подписки на календари + * @param {number} [ttl=60000] - Time to live кеша в мс + * @returns {Promise} Массив подписок + */ +export async function getMySubscriptions(ttl = 60000) { + const key = 'subscriptions:my'; + + return dataManager.fetch( + key, + async () => { + const url = new URL('/api/v1/calendar/subscriptions/', window.location.origin); + + const response = await fetch(url.toString(), { + headers: authHeaders(false) + }); + + if (response.status === 401) { + console.warn('[CalendarsAPI] 401 Unauthorized'); + return []; + } + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + return data.results || []; + }, + ttl + ); +} + +/** + * Инвалидировать весь кеш календарей + */ +export function invalidateCalendarsCache() { + dataManager.invalidatePattern(/^calendars:/); + dataManager.invalidatePattern(/^subscriptions:/); +} diff --git a/backend/static/js/components/calendarManageModal.js b/backend/static/js/components/calendarManageModal.js new file mode 100644 index 00000000..4818117e --- /dev/null +++ b/backend/static/js/components/calendarManageModal.js @@ -0,0 +1,272 @@ +/** + * @fileoverview Calendar Manage Modal - модальное окно создания/редактирования календаря + * @module components/calendarManageModal + */ + +import { createCalendar, updateCalendar, deleteCalendar } from '../api/calendarsApi.js'; +import { getMyDepartments } from '../api/departmentsApi.js'; + +/** + * Инициализация модального окна управления календарём + * @param {Object} options - Параметры + * @param {Function} options.onSuccess - Callback при успешном сохранении/удалении + * @returns {Object} API модального окна + */ +export function initCalendarManageModal(options = {}) { + const { onSuccess = () => {} } = options; + + const modal = document.getElementById('calendarManageModal'); + if (!modal) { + console.warn('[CalendarManageModal] Modal element not found'); + return null; + } + + const bsModal = new bootstrap.Modal(modal); + + // Elements + const form = document.getElementById('calendarManageForm'); + const titleEl = document.getElementById('calendarManageModalTitle'); + const calendarIdInput = document.getElementById('calendarId'); + const titleInput = document.getElementById('calendarTitle'); + const descInput = document.getElementById('calendarDescription'); + const colorInput = document.getElementById('calendarColor'); + const colorTextInput = document.getElementById('calendarColorText'); + const typeInputs = document.querySelectorAll('input[name="calendar_type"]'); + const deptSelect = document.getElementById('calendarDepartment'); + const deptSelectWrapper = document.getElementById('calendarDepartmentSelect'); + const autoSubscribeDeptWrapper = document.getElementById('calendarAutoSubscribeDeptWrapper'); + const saveBtn = document.getElementById('calendarSaveBtn'); + const deleteBtn = document.getElementById('calendarDeleteBtn'); + const errorEl = document.getElementById('calendarManageError'); + + let currentCalendar = null; + + /** + * Открыть модальное окно для создания календаря + */ + function openForCreate() { + currentCalendar = null; + titleEl.textContent = 'Создать календарь'; + deleteBtn.classList.add('d-none'); + resetForm(); + bsModal.show(); + } + + /** + * Открыть модальное окно для редактирования календаря + * @param {Object} calendar - Данные календаря + */ + function openForEdit(calendar) { + currentCalendar = calendar; + titleEl.textContent = 'Редактировать календарь'; + deleteBtn.classList.remove('d-none'); + + // Заполнить форму + calendarIdInput.value = calendar.id; + titleInput.value = calendar.title; + descInput.value = calendar.description || ''; + colorInput.value = calendar.color || '#0d6efd'; + colorTextInput.value = calendar.color || '#0d6efd'; + + // Определить тип + let type = 'personal'; + if (calendar.is_global) { + type = 'public'; + } else if (calendar.is_department) { + type = 'department'; + } + + typeInputs.forEach(input => { + input.checked = input.value === type; + }); + + // Если отдел, выбрать его + if (calendar.owner_department) { + deptSelect.value = calendar.owner_department; + } + + // Настройки + document.getElementById('calendarAutoSubscribeNewUsers').checked = + calendar.auto_subscribe_new_users || false; + document.getElementById('calendarAutoSubscribeDept').checked = + calendar.auto_subscribe_department_members || false; + + updateTypeVisibility(); + bsModal.show(); + } + + /** + * Сбросить форму + */ + function resetForm() { + form.reset(); + calendarIdInput.value = ''; + colorInput.value = '#0d6efd'; + colorTextInput.value = '#0d6efd'; + errorEl.classList.add('d-none'); + updateTypeVisibility(); + } + + /** + * Обновить видимость полей в зависимости от типа + */ + function updateTypeVisibility() { + const selectedType = document.querySelector('input[name="calendar_type"]:checked')?.value; + + if (selectedType === 'department') { + deptSelectWrapper.classList.remove('d-none'); + autoSubscribeDeptWrapper.classList.remove('d-none'); + } else { + deptSelectWrapper.classList.add('d-none'); + autoSubscribeDeptWrapper.classList.add('d-none'); + } + } + + /** + * Загрузить список отделов + */ + async function loadDepartments() { + try { + const departments = await getMyDepartments(); + + // Заполнить select + deptSelect.innerHTML = '' + + departments.map(dept => + `` + ).join(''); + + } catch (error) { + console.error('[CalendarManageModal] Failed to load departments:', error); + } + } + + /** + * Собрать данные формы + * @returns {Object} + */ + function getFormData() { + const data = { + title: titleInput.value.trim(), + description: descInput.value.trim(), + color: colorInput.value, + auto_subscribe_new_users: document.getElementById('calendarAutoSubscribeNewUsers').checked, + auto_subscribe_department_members: document.getElementById('calendarAutoSubscribeDept').checked + }; + + const type = document.querySelector('input[name="calendar_type"]:checked')?.value; + + if (type === 'personal') { + data.visibility = 'private'; + // owner_user будет установлен автоматически на backend + } else if (type === 'public') { + data.visibility = 'public'; + } else if (type === 'department') { + data.visibility = 'department'; + data.owner_department = parseInt(deptSelect.value); + + if (!data.owner_department) { + throw new Error('Выберите отдел'); + } + } + + return data; + } + + /** + * Сохранить календарь + */ + async function save() { + try { + errorEl.classList.add('d-none'); + saveBtn.disabled = true; + + const data = getFormData(); + + if (currentCalendar) { + // Обновление + await updateCalendar(currentCalendar.id, data); + } else { + // Создание + await createCalendar(data); + } + + bsModal.hide(); + onSuccess(); + + } catch (error) { + console.error('[CalendarManageModal] Save failed:', error); + errorEl.textContent = error.message || 'Не удалось сохранить календарь'; + errorEl.classList.remove('d-none'); + } finally { + saveBtn.disabled = false; + } + } + + /** + * Удалить календарь + */ + async function remove() { + if (!currentCalendar) return; + + if (!confirm(`Удалить календарь "${currentCalendar.title}"?\n\nВсе события этого календаря будут удалены.`)) { + return; + } + + try { + deleteBtn.disabled = true; + await deleteCalendar(currentCalendar.id); + bsModal.hide(); + onSuccess(); + } catch (error) { + console.error('[CalendarManageModal] Delete failed:', error); + errorEl.textContent = error.message || 'Не удалось удалить календарь'; + errorEl.classList.remove('d-none'); + } finally { + deleteBtn.disabled = false; + } + } + + // Event listeners + + // Синхронизация цвета + colorInput.addEventListener('input', (e) => { + colorTextInput.value = e.target.value; + }); + + colorTextInput.addEventListener('input', (e) => { + const value = e.target.value; + if (/^#[0-9A-Fa-f]{6}$/.test(value)) { + colorInput.value = value; + } + }); + + // Изменение типа календаря + typeInputs.forEach(input => { + input.addEventListener('change', updateTypeVisibility); + }); + + // Сохранить + saveBtn.addEventListener('click', save); + + // Удалить + deleteBtn.addEventListener('click', remove); + + // Сброс при закрытии + modal.addEventListener('hidden.bs.modal', resetForm); + + // Слушать событие редактирования из других компонентов + document.addEventListener('calendar:edit', (e) => { + openForEdit(e.detail); + }); + + // Инициализация + loadDepartments(); + + // Public API + return { + openForCreate, + openForEdit, + show: () => bsModal.show(), + hide: () => bsModal.hide() + }; +} diff --git a/backend/static/js/components/calendarManager.js b/backend/static/js/components/calendarManager.js new file mode 100644 index 00000000..ac5177d7 --- /dev/null +++ b/backend/static/js/components/calendarManager.js @@ -0,0 +1,289 @@ +/** + * @fileoverview Calendar Manager Component - управление списком календарей + * @module components/calendarManager + */ + +import { + getMyCalendars, + subscribeToCalendar, + unsubscribeFromCalendar, + invalidateCalendarsCache +} from '../api/calendarsApi.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(); + + /** + * Загрузить список календарей + */ + async function loadCalendars() { + try { + calendars = await getMyCalendars(); + + // По умолчанию все календари видимы + visibleCalendarIds = new Set(calendars.map(cal => cal.id)); + + render(); + onCalendarsChange(calendars, Array.from(visibleCalendarIds)); + } catch (error) { + console.error('[CalendarManager] Failed to load calendars:', error); + container.innerHTML = ` +
+ + Не удалось загрузить календари +
+ `; + } + } + + /** + * Переключить видимость календаря + * @param {number} calendarId + */ + function toggleCalendarVisibility(calendarId) { + if (visibleCalendarIds.has(calendarId)) { + visibleCalendarIds.delete(calendarId); + } else { + visibleCalendarIds.add(calendarId); + } + + render(); + onCalendarToggle(calendarId, visibleCalendarIds.has(calendarId)); + } + + /** + * Подписаться на календарь + * @param {number} calendarId + */ + async function subscribe(calendarId) { + try { + await subscribeToCalendar(calendarId); + await loadCalendars(); // Перезагрузить список + } catch (error) { + console.error('[CalendarManager] Subscribe failed:', error); + alert('Не удалось подписаться на календарь'); + } + } + + /** + * Отписаться от календаря + * @param {number} calendarId + */ + async function unsubscribe(calendarId) { + if (!confirm('Отписаться от этого календаря?')) return; + + try { + await unsubscribeFromCalendar(calendarId); + await loadCalendars(); // Перезагрузить список + } catch (error) { + console.error('[CalendarManager] Unsubscribe failed:', error); + alert('Не удалось отписаться от календаря'); + } + } + + /** + * Получить иконку для типа календаря + * @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; + } + + const html = calendars.map(calendar => { + const isVisible = visibleCalendarIds.has(calendar.id); + const canEdit = calendar.user_can_edit; + const isSubscribed = calendar.is_subscribed; + + return ` +
+
+ + +
+ +
+ ${canEdit ? ` + + ` : ''} + + ${!isSubscribed ? ` + + ` : ` + + `} +
+
+ `; + }).join(''); + + container.innerHTML = html; + attachEventListeners(); + } + + /** + * Прикрепить обработчики событий + */ + function attachEventListeners() { + // Переключение видимости + container.querySelectorAll('.calendar-visibility-toggle').forEach(checkbox => { + checkbox.addEventListener('change', (e) => { + const calendarId = parseInt(e.target.dataset.calendarId); + toggleCalendarVisibility(calendarId); + }); + }); + + // Подписка + container.querySelectorAll('.calendar-subscribe-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const calendarId = parseInt(e.currentTarget.dataset.calendarId); + subscribe(calendarId); + }); + }); + + // Отписка + container.querySelectorAll('.calendar-unsubscribe-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const calendarId = parseInt(e.currentTarget.dataset.calendarId); + unsubscribe(calendarId); + }); + }); + + // Редактирование (делегируем наверх) + container.querySelectorAll('.calendar-edit-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const calendarId = parseInt(e.currentTarget.dataset.calendarId); + 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(); + 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(); + + // Public API + return { + refresh, + getVisibleCalendarIds, + setVisibleCalendars, + loadCalendars + }; +} diff --git a/backend/static/scss/components/calendar-manager.scss b/backend/static/scss/components/calendar-manager.scss new file mode 100644 index 00000000..b8d2c083 --- /dev/null +++ b/backend/static/scss/components/calendar-manager.scss @@ -0,0 +1,186 @@ +// ============================================================================= +// Calendar Manager Component Styles +// ============================================================================= +// Стили для компонента управления списком календарей + +// ────────────────────────────────────────────────────────────── +// Calendar List Container +// ────────────────────────────────────────────────────────────── +.calendar-list-container { + max-height: 400px; + overflow-y: auto; + padding: var(--space-2); + + // Кастомный scrollbar + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 3px; + } + + &::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + + &:hover { + background: rgba(255, 255, 255, 0.3); + } + } +} + +// ────────────────────────────────────────────────────────────── +// Calendar List Item +// ────────────────────────────────────────────────────────────── +.calendar-list-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2); + padding: var(--space-2); + border-radius: var(--radius-md); + transition: background-color 0.2s ease; + + &:hover { + background-color: rgba(255, 255, 255, 0.05); + } + + // Чекбокс и лейбл + .form-check { + flex: 1; + min-width: 0; + margin-bottom: 0; + } + + .form-check-label { + cursor: pointer; + user-select: none; + min-width: 0; + } + + // Цветовой индикатор + .calendar-color-indicator { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; + flex-shrink: 0; + border: 2px solid rgba(255, 255, 255, 0.2); + } + + // Иконка календаря + .calendar-icon { + flex-shrink: 0; + font-size: 0.9rem; + } + + // Название календаря + .calendar-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + // Действия + .calendar-list-item-actions { + display: flex; + gap: var(--space-1); + flex-shrink: 0; + opacity: 0; + transition: opacity 0.2s ease; + } + + &:hover .calendar-list-item-actions { + opacity: 1; + } + + // Кнопки действий + .btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; + line-height: 1; + + i { + font-size: 0.875rem; + } + } +} + +// ────────────────────────────────────────────────────────────── +// Calendar Chooser Button (в header) +// ────────────────────────────────────────────────────────────── +#calendarChooserBtn { + position: relative; + + &::after { + margin-left: 0.5rem; + } +} + +// ────────────────────────────────────────────────────────────── +// Calendar Header Actions +// ────────────────────────────────────────────────────────────── +.calendar-header-actions { + display: flex; + gap: var(--space-2); + align-items: center; +} + +// ────────────────────────────────────────────────────────────── +// Empty State +// ────────────────────────────────────────────────────────────── +.calendar-list-empty { + text-align: center; + padding: var(--space-4); + color: var(--bs-secondary); + + i { + font-size: 2rem; + display: block; + margin-bottom: var(--space-2); + opacity: 0.5; + } + + small { + font-size: 0.875rem; + } +} + +// ────────────────────────────────────────────────────────────── +// Модификаторы +// ────────────────────────────────────────────────────────────── + +// Компактный вид +.calendar-list-item--compact { + padding: var(--space-1) var(--space-2); + + .calendar-color-indicator { + width: 10px; + height: 10px; + } + + .calendar-icon { + font-size: 0.8rem; + } +} + +// Отключённый календарь +.calendar-list-item--disabled { + opacity: 0.5; + pointer-events: none; +} + +// ────────────────────────────────────────────────────────────── +// Responsive +// ────────────────────────────────────────────────────────────── +@media (max-width: 991.98px) { + .calendar-list-container { + max-height: 300px; + } + + .calendar-list-item-actions { + opacity: 1; // На мобильных всегда показываем кнопки + } +} diff --git a/backend/static/scss/main.scss b/backend/static/scss/main.scss index 0a300189..fe3ba10e 100644 --- a/backend/static/scss/main.scss +++ b/backend/static/scss/main.scss @@ -73,6 +73,7 @@ // Календарь и rightbar @import 'components/rightbar-calendar'; +@import 'components/calendar-manager'; // ============================================================================= // 4. UTILITIES - Вспомогательные классы diff --git a/backend/templates/includes/components/calendar_desktop.html b/backend/templates/includes/components/calendar_desktop.html index 95626f25..5d7966d5 100644 --- a/backend/templates/includes/components/calendar_desktop.html +++ b/backend/templates/includes/components/calendar_desktop.html @@ -4,29 +4,58 @@
-
Календарь
+
Календари
- {# Выпадающий выбор: Компания / Отделы / Личный #} - + {# Кнопка управления календарями #} +
События - + + +
+
+ + {# Список календарей (сворачиваемый) #} +
+
+
+ {# Здесь будет JS-генерируемый список календарей #} +
+
+ Загрузка... +
+
+
diff --git a/backend/templates/includes/components/calendar_modal_manage.html b/backend/templates/includes/components/calendar_modal_manage.html new file mode 100644 index 00000000..966779fb --- /dev/null +++ b/backend/templates/includes/components/calendar_modal_manage.html @@ -0,0 +1,176 @@ +{# Модальное окно управления календарём #} + diff --git a/backend/templates/includes/rightbar_calendar.html b/backend/templates/includes/rightbar_calendar.html index 66a72ce9..a7daf8ad 100644 --- a/backend/templates/includes/rightbar_calendar.html +++ b/backend/templates/includes/rightbar_calendar.html @@ -10,6 +10,7 @@ {# Модальные окна #} {% include "includes/components/calendar_modal_create.html" %} {% include "includes/components/calendar_modal_details.html" %} +{% include "includes/components/calendar_modal_manage.html" %} {% include "includes/components/calendar_context_menu.html" %} {# JavaScript инициализация #}