Corp/backend/static/js/components/calendarManageModal.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

273 lines
8.5 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 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 = '<option value="">Выберите отдел...</option>' +
departments.map(dept =>
`<option value="${dept.id}">${dept.name}</option>`
).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()
};
}