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
This commit is contained in:
parent
a29aa69d16
commit
826298ff08
312
backend/static/js/api/calendarsApi.js
Normal file
312
backend/static/js/api/calendarsApi.js
Normal file
@ -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<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:/);
|
||||
}
|
||||
272
backend/static/js/components/calendarManageModal.js
Normal file
272
backend/static/js/components/calendarManageModal.js
Normal file
@ -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 = '<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()
|
||||
};
|
||||
}
|
||||
289
backend/static/js/components/calendarManager.js
Normal file
289
backend/static/js/components/calendarManager.js
Normal file
@ -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 = `
|
||||
<div class="alert alert-danger alert-sm">
|
||||
<i class="bi-exclamation-triangle me-2"></i>
|
||||
Не удалось загрузить календари
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключить видимость календаря
|
||||
* @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 '<i class="bi-person-fill"></i>';
|
||||
} else if (calendar.is_department) {
|
||||
return '<i class="bi-building"></i>';
|
||||
} else if (calendar.is_global) {
|
||||
return '<i class="bi-globe"></i>';
|
||||
}
|
||||
return '<i class="bi-calendar3"></i>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить бейдж владельца
|
||||
* @param {Object} calendar
|
||||
* @returns {string} HTML бейджа
|
||||
*/
|
||||
function getOwnerBadge(calendar) {
|
||||
if (calendar.is_personal && calendar.owner_user_name) {
|
||||
return `<span class="badge bg-secondary-subtle text-secondary-emphasis rounded-pill ms-auto">${calendar.owner_user_name}</span>`;
|
||||
} else if (calendar.is_department && calendar.owner_department_name) {
|
||||
return `<span class="badge bg-info-subtle text-info-emphasis rounded-pill ms-auto">${calendar.owner_department_name}</span>`;
|
||||
} else if (calendar.is_global) {
|
||||
return `<span class="badge bg-primary-subtle text-primary-emphasis rounded-pill ms-auto">Общий</span>`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Рендер списка календарей
|
||||
*/
|
||||
function render() {
|
||||
if (calendars.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-muted text-center py-3">
|
||||
<i class="bi-calendar3 fs-4 d-block mb-2"></i>
|
||||
<small>Нет доступных календарей</small>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const html = calendars.map(calendar => {
|
||||
const isVisible = visibleCalendarIds.has(calendar.id);
|
||||
const canEdit = calendar.user_can_edit;
|
||||
const isSubscribed = calendar.is_subscribed;
|
||||
|
||||
return `
|
||||
<div class="calendar-list-item" data-calendar-id="${calendar.id}">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input calendar-visibility-toggle"
|
||||
type="checkbox"
|
||||
id="cal_${calendar.id}"
|
||||
${isVisible ? 'checked' : ''}
|
||||
data-calendar-id="${calendar.id}"
|
||||
>
|
||||
<label class="form-check-label d-flex align-items-center gap-2 w-100" for="cal_${calendar.id}">
|
||||
<span class="calendar-color-indicator" style="background-color: ${calendar.color}"></span>
|
||||
<span class="calendar-icon text-muted">${getCalendarIcon(calendar)}</span>
|
||||
<span class="calendar-title flex-grow-1">${calendar.title}</span>
|
||||
${getOwnerBadge(calendar)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="calendar-list-item-actions">
|
||||
${canEdit ? `
|
||||
<button
|
||||
class="btn btn-sm btn-outline-secondary calendar-edit-btn"
|
||||
data-calendar-id="${calendar.id}"
|
||||
title="Редактировать"
|
||||
>
|
||||
<i class="bi-pencil"></i>
|
||||
</button>
|
||||
` : ''}
|
||||
|
||||
${!isSubscribed ? `
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary calendar-subscribe-btn"
|
||||
data-calendar-id="${calendar.id}"
|
||||
title="Подписаться"
|
||||
>
|
||||
<i class="bi-plus-lg"></i>
|
||||
</button>
|
||||
` : `
|
||||
<button
|
||||
class="btn btn-sm btn-outline-danger calendar-unsubscribe-btn"
|
||||
data-calendar-id="${calendar.id}"
|
||||
title="Отписаться"
|
||||
>
|
||||
<i class="bi-dash-lg"></i>
|
||||
</button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).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<number>}
|
||||
*/
|
||||
function getVisibleCalendarIds() {
|
||||
return Array.from(visibleCalendarIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Установить видимость календарей
|
||||
* @param {Array<number>} calendarIds
|
||||
*/
|
||||
function setVisibleCalendars(calendarIds) {
|
||||
visibleCalendarIds = new Set(calendarIds);
|
||||
render();
|
||||
onCalendarsChange(calendars, Array.from(visibleCalendarIds));
|
||||
}
|
||||
|
||||
// Инициализация
|
||||
loadCalendars();
|
||||
|
||||
// Public API
|
||||
return {
|
||||
refresh,
|
||||
getVisibleCalendarIds,
|
||||
setVisibleCalendars,
|
||||
loadCalendars
|
||||
};
|
||||
}
|
||||
186
backend/static/scss/components/calendar-manager.scss
Normal file
186
backend/static/scss/components/calendar-manager.scss
Normal file
@ -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; // На мобильных всегда показываем кнопки
|
||||
}
|
||||
}
|
||||
@ -73,6 +73,7 @@
|
||||
|
||||
// Календарь и rightbar
|
||||
@import 'components/rightbar-calendar';
|
||||
@import 'components/calendar-manager';
|
||||
|
||||
// =============================================================================
|
||||
// 4. UTILITIES - Вспомогательные классы
|
||||
|
||||
@ -4,29 +4,58 @@
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
|
||||
<div class="d-flex align-items-center gap-2 flex-shrink-1 min-w-0">
|
||||
<h6 class="card-title fw-semibold mb-0">Календарь</h6>
|
||||
<h6 class="card-title fw-semibold mb-0">Календари</h6>
|
||||
|
||||
{# Выпадающий выбор: Компания / Отделы / Личный #}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle text-truncate" type="button"
|
||||
id="calendarChooserBtn" data-bs-toggle="dropdown" aria-expanded="false"
|
||||
style="max-width: 150px;">
|
||||
Компания
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="calendarChooserBtn" id="calendarChooserMenu">
|
||||
<li><button class="dropdown-item" type="button" data-cal="personal">Личный</button></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><button class="dropdown-item" type="button" data-cal="company">Компания</button></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{# пункты отделов добавим из JS по data-dept-id #}
|
||||
</ul>
|
||||
</div>
|
||||
{# Кнопка управления календарями #}
|
||||
<button
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#calendarListCollapse"
|
||||
aria-expanded="false"
|
||||
aria-controls="calendarListCollapse"
|
||||
title="Управление календарями"
|
||||
>
|
||||
<i class="bi-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2 flex-shrink-0">
|
||||
<span class="card-sub text-muted d-none d-xl-inline">События</span>
|
||||
<button class="btn btn-sm btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#eventCreateModal"
|
||||
aria-label="Создать событие"><i class="bi-plus-lg"></i></button>
|
||||
<button
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#calendarManageModal"
|
||||
aria-label="Создать календарь"
|
||||
title="Создать календарь"
|
||||
>
|
||||
<i class="bi-plus-lg"></i>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#eventCreateModal"
|
||||
aria-label="Создать событие"
|
||||
title="Создать событие"
|
||||
>
|
||||
<i class="bi-calendar-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Список календарей (сворачиваемый) #}
|
||||
<div class="collapse" id="calendarListCollapse">
|
||||
<div class="card-body border-bottom">
|
||||
<div id="calendarListContainer" class="calendar-list-container">
|
||||
{# Здесь будет JS-генерируемый список календарей #}
|
||||
<div class="text-center py-3">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="visually-hidden">Загрузка...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
176
backend/templates/includes/components/calendar_modal_manage.html
Normal file
176
backend/templates/includes/components/calendar_modal_manage.html
Normal file
@ -0,0 +1,176 @@
|
||||
{# Модальное окно управления календарём #}
|
||||
<div class="modal fade" id="calendarManageModal" tabindex="-1" aria-labelledby="calendarManageModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="calendarManageModalLabel">
|
||||
<i class="bi-calendar3 me-2"></i>
|
||||
<span id="calendarManageModalTitle">Создать календарь</span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<form id="calendarManageForm">
|
||||
<input type="hidden" id="calendarId" name="calendar_id" value="">
|
||||
|
||||
{# Название #}
|
||||
<div class="mb-3">
|
||||
<label for="calendarTitle" class="form-label">
|
||||
Название <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="calendarTitle"
|
||||
name="title"
|
||||
required
|
||||
placeholder="Например: Мой календарь"
|
||||
maxlength="200"
|
||||
>
|
||||
</div>
|
||||
|
||||
{# Описание #}
|
||||
<div class="mb-3">
|
||||
<label for="calendarDescription" class="form-label">Описание</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="calendarDescription"
|
||||
name="description"
|
||||
rows="3"
|
||||
placeholder="Краткое описание календаря..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
{# Цвет #}
|
||||
<div class="mb-3">
|
||||
<label for="calendarColor" class="form-label">Цвет</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="color"
|
||||
class="form-control form-control-color"
|
||||
id="calendarColor"
|
||||
name="color"
|
||||
value="#0d6efd"
|
||||
title="Выберите цвет календаря"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="calendarColorText"
|
||||
value="#0d6efd"
|
||||
pattern="^#[0-9A-Fa-f]{6}$"
|
||||
maxlength="7"
|
||||
>
|
||||
</div>
|
||||
<div class="form-text">Используется для выделения событий</div>
|
||||
</div>
|
||||
|
||||
{# Тип календаря #}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Тип календаря <span class="text-danger">*</span></label>
|
||||
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="calendar_type"
|
||||
id="calendarTypePersonal"
|
||||
value="personal"
|
||||
checked
|
||||
>
|
||||
<label class="form-check-label" for="calendarTypePersonal">
|
||||
<i class="bi-person-fill me-1"></i>
|
||||
Личный календарь
|
||||
</label>
|
||||
<div class="form-text ms-4">Только вы можете видеть и редактировать</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-2">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="calendar_type"
|
||||
id="calendarTypePublic"
|
||||
value="public"
|
||||
>
|
||||
<label class="form-check-label" for="calendarTypePublic">
|
||||
<i class="bi-globe me-1"></i>
|
||||
Публичный календарь
|
||||
</label>
|
||||
<div class="form-text ms-4">Все пользователи могут видеть</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mt-2">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="calendar_type"
|
||||
id="calendarTypeDepartment"
|
||||
value="department"
|
||||
>
|
||||
<label class="form-check-label" for="calendarTypeDepartment">
|
||||
<i class="bi-building me-1"></i>
|
||||
Календарь отдела
|
||||
</label>
|
||||
<div class="form-text ms-4">Члены отдела могут видеть</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Выбор отдела (показывается только для типа "department") #}
|
||||
<div class="mb-3 d-none" id="calendarDepartmentSelect">
|
||||
<label for="calendarDepartment" class="form-label">
|
||||
Отдел <span class="text-danger">*</span>
|
||||
</label>
|
||||
<select class="form-select" id="calendarDepartment" name="owner_department">
|
||||
<option value="">Выберите отдел...</option>
|
||||
{# Будет заполнено из JS #}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Дополнительные настройки #}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Дополнительные настройки</label>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="calendarAutoSubscribeNewUsers"
|
||||
name="auto_subscribe_new_users"
|
||||
>
|
||||
<label class="form-check-label" for="calendarAutoSubscribeNewUsers">
|
||||
Автоматически подписывать новых пользователей
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch mt-2 d-none" id="calendarAutoSubscribeDeptWrapper">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="calendarAutoSubscribeDept"
|
||||
name="auto_subscribe_department_members"
|
||||
>
|
||||
<label class="form-check-label" for="calendarAutoSubscribeDept">
|
||||
Автоматически подписывать членов отдела
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Сообщение об ошибке #}
|
||||
<div id="calendarManageError" class="alert alert-danger d-none" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-danger d-none" id="calendarDeleteBtn">
|
||||
<i class="bi-trash me-1"></i> Удалить
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="calendarSaveBtn">
|
||||
<i class="bi-check-lg me-1"></i> Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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 инициализация #}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user