Corp/backend/static/js/api/calendarsApi.js
Игорь Надеин 826298ff08 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
2026-02-11 09:42:18 +03:00

313 lines
9.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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<Array>} Массив календарей
*/
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<Object>} Календарь
*/
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<Object>} Созданный календарь
*/
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<Object>} Обновленный календарь
*/
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<void>}
*/
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<Object>} Данные подписки
*/
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<void>}
*/
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<Array>} Массив подписок
*/
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:/);
}