// document-fields.js - Скрипт для управления полями документа в задачах // Показывает кнопку "Реквизиты" только для задач с типом "document", // только когда задача раскрыта и только для пользователей из групп "Подписант" или "Секретарь" (function() { 'use strict'; // Конфигурация const CONFIG = { modalId: 'documentFieldsModal', signerGroup: 'Подписант', secretaryGroup: 'Секретарь', apiEndpoint: '/api2/idusers', usersEndpoint: '/api/users', modalStyles: ` `, buttonStyles: ` ` }; // Текущий пользователь let currentUser = null; // Группы пользователя (все группы из разных источников) let userGroups = []; // Кэш для типов задач const taskTypeCache = new Map(); // Множество задач, для которых уже выполняется проверка const pendingChecks = new Set(); // Селекторы для поиска раскрытых задач const EXPANDED_TASK_SELECTORS = [ '.task-card.expanded', '.task-item.expanded', '[data-expanded="true"]', '.task-details', '.task-content.expanded' ]; // Состояние календаря let calendarState = { currentDate: new Date(), selectedDate: null, isOpen: false, inputElement: null }; // Получение текущего пользователя async function getCurrentUser() { try { const response = await fetch('/api/user'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data.user) { currentUser = data.user; console.log('✅ DocumentFields: текущий пользователь', currentUser.login); // Получаем все группы пользователя await getAllUserGroups(currentUser.login || currentUser.id); } return currentUser; } catch (error) { console.error('❌ DocumentFields: ошибка получения пользователя', error); return null; } } // Получение ВСЕХ групп пользователя из разных источников async function getAllUserGroups(userLogin) { const allGroups = new Set(); try { // 1. Пробуем получить через API /api2/idusers await fetchGroupsFromApi2(userLogin, allGroups); // 2. Пробуем получить через API /api/users await fetchGroupsFromUsersApi(userLogin, allGroups); // 3. Проверяем, есть ли группы в currentUser if (currentUser) { addGroupsFromCurrentUser(allGroups); } // Преобразуем Set в массив и сохраняем userGroups = Array.from(allGroups); console.log('✅ DocumentFields: ВСЕ группы пользователя:', userGroups); // Детальный вывод для отладки console.log('🔍 Проверка доступа:'); console.log(' - Группы:', userGroups); console.log(' - Ищем "Подписант":', userGroups.some(g => g.toLowerCase().includes(CONFIG.signerGroup.toLowerCase()))); console.log(' - Ищем "Секретарь":', userGroups.some(g => g.toLowerCase().includes(CONFIG.secretaryGroup.toLowerCase()))); return userGroups; } catch (error) { console.error('❌ DocumentFields: ошибка получения групп', error); return []; } } // Получение групп из /api2/idusers async function fetchGroupsFromApi2(userLogin, groupsSet) { try { const response = await fetch(`${CONFIG.apiEndpoint}?login=${userLogin}`); if (!response.ok) { console.warn('⚠️ DocumentFields: API /api2/idusers вернул ошибку', response.status); return; } const data = await response.json(); console.log('📦 Данные из /api2/idusers:', data); // Обрабатываем массив пользователей if (Array.isArray(data)) { const userData = data.find(u => u.login === userLogin || u.user_login === userLogin || u.name === userLogin || u.user_name === userLogin ); if (userData) { extractGroupsFromUserData(userData, groupsSet); } } // Обрабатываем объект пользователя else if (data && typeof data === 'object') { extractGroupsFromUserData(data, groupsSet); } } catch (error) { console.error('❌ DocumentFields: ошибка fetchGroupsFromApi2', error); } } // Получение групп из /api/users async function fetchGroupsFromUsersApi(userLogin, groupsSet) { try { const response = await fetch(`${CONFIG.usersEndpoint}?login=${userLogin}`); if (!response.ok) { console.warn('⚠️ DocumentFields: API /api/users вернул ошибку', response.status); return; } const data = await response.json(); console.log('📦 Данные из /api/users:', data); // Обрабатываем данные в зависимости от формата if (Array.isArray(data)) { const userData = data.find(u => u.login === userLogin || u.username === userLogin || u.email === userLogin ); if (userData) { extractGroupsFromUserData(userData, groupsSet); } } else if (data && typeof data === 'object') { extractGroupsFromUserData(data, groupsSet); } } catch (error) { console.error('❌ DocumentFields: ошибка fetchGroupsFromUsersApi', error); } } // Извлечение групп из данных пользователя function extractGroupsFromUserData(userData, groupsSet) { if (!userData) return; console.log('🔍 Извлекаем группы из:', userData); // 1. Проверяем group_name (может быть строкой или массивом) if (userData.group_name) { if (Array.isArray(userData.group_name)) { userData.group_name.forEach(g => { if (g) groupsSet.add(String(g).trim()); }); } else if (typeof userData.group_name === 'string') { // Может быть строкой с разделителями const groups = userData.group_name.split(/[,;|]/).map(g => g.trim()); groups.forEach(g => { if (g) groupsSet.add(g); }); } } // 2. Проверяем ldap_group if (userData.ldap_group) { if (Array.isArray(userData.ldap_group)) { userData.ldap_group.forEach(g => { if (g) groupsSet.add(String(g).trim()); }); } else if (typeof userData.ldap_group === 'string') { groupsSet.add(userData.ldap_group.trim()); } } // 3. Проверяем metadata.groups if (userData.metadata?.groups) { if (Array.isArray(userData.metadata.groups)) { userData.metadata.groups.forEach(g => { if (g) groupsSet.add(String(g).trim()); }); } } // 4. Проверяем просто groups (если есть) if (userData.groups) { if (Array.isArray(userData.groups)) { userData.groups.forEach(g => { if (g) groupsSet.add(String(g).trim()); }); } else if (typeof userData.groups === 'string') { const groups = userData.groups.split(/[,;|]/).map(g => g.trim()); groups.forEach(g => { if (g) groupsSet.add(g); }); } } // 5. Проверяем roles (если есть) if (userData.roles) { if (Array.isArray(userData.roles)) { userData.roles.forEach(r => { if (r) groupsSet.add(String(r).trim()); }); } } // 6. Проверяем department (может содержать группу) if (userData.department) { groupsSet.add(String(userData.department).trim()); } // 7. Проверяем position (может содержать группу) if (userData.position) { groupsSet.add(String(userData.position).trim()); } } // Добавление групп из currentUser function addGroupsFromCurrentUser(groupsSet) { if (!currentUser) return; // Проверяем различные поля в currentUser const fieldsToCheck = [ 'group', 'groups', 'role', 'roles', 'department', 'position', 'user_group', 'user_groups' ]; fieldsToCheck.forEach(field => { if (currentUser[field]) { if (Array.isArray(currentUser[field])) { currentUser[field].forEach(g => { if (g) groupsSet.add(String(g).trim()); }); } else if (typeof currentUser[field] === 'string') { groupsSet.add(currentUser[field].trim()); } } }); } // Проверка, имеет ли пользователь доступ (Подписант или Секретарь) function hasUserAccess() { if (!userGroups || userGroups.length === 0) { console.log('❌ DocumentFields: у пользователя нет групп'); return false; } // Приводим искомые группы к нижнему регистру const signerGroupLower = CONFIG.signerGroup.toLowerCase(); const secretaryGroupLower = CONFIG.secretaryGroup.toLowerCase(); // Проверяем КАЖДУЮ группу пользователя for (const group of userGroups) { if (!group) continue; const groupLower = String(group).toLowerCase().trim(); // Точное совпадение if (groupLower === signerGroupLower || groupLower === secretaryGroupLower) { console.log(`✅ Найдено точное совпадение: "${group}"`); return true; } // Частичное совпадение (содержит подстроку) if (groupLower.includes(signerGroupLower) || groupLower.includes(secretaryGroupLower)) { console.log(`✅ Найдено частичное совпадение: "${group}" содержит искомую группу`); return true; } // Совпадение по словам (если группа состоит из нескольких слов) const groupWords = groupLower.split(/[\s\-_]/); const signerWords = signerGroupLower.split(/[\s\-_]/); const secretaryWords = secretaryGroupLower.split(/[\s\-_]/); // Проверяем, содержит ли группа все слова из искомой группы const matchesSigner = signerWords.every(word => groupLower.includes(word) || groupWords.some(gw => gw.includes(word)) ); const matchesSecretary = secretaryWords.every(word => groupLower.includes(word) || groupWords.some(gw => gw.includes(word)) ); if (matchesSigner || matchesSecretary) { console.log(`✅ Найдено совпадение по словам: "${group}"`); return true; } } console.log('❌ DocumentFields: пользователь НЕ имеет доступа'); console.log(' Группы пользователя:', userGroups); console.log(' Нужные группы:', [CONFIG.signerGroup, CONFIG.secretaryGroup]); return false; } // Функция для отладки - показывает все группы пользователя function debugUserGroups() { console.log('=== ОТЛАДКА ГРУПП ПОЛЬЗОВАТЕЛЯ ==='); console.log('Текущий пользователь:', currentUser); console.log('Все группы:', userGroups); console.log('Ищем "Подписант":', userGroups.filter(g => g.toLowerCase().includes(CONFIG.signerGroup.toLowerCase()) )); console.log('Ищем "Секретарь":', userGroups.filter(g => g.toLowerCase().includes(CONFIG.secretaryGroup.toLowerCase()) )); console.log('Доступ:', hasUserAccess()); console.log('================================'); return { user: currentUser, groups: userGroups, hasAccess: hasUserAccess() }; } // Получение типа задачи по ID async function getTaskType(taskId) { // Проверяем кэш if (taskTypeCache.has(taskId)) { return taskTypeCache.get(taskId); } // Проверяем, не выполняется ли уже проверка для этой задачи if (pendingChecks.has(taskId)) { // Ждем завершения проверки return new Promise((resolve) => { const checkInterval = setInterval(() => { if (taskTypeCache.has(taskId)) { clearInterval(checkInterval); resolve(taskTypeCache.get(taskId)); } }, 100); // Таймаут на случай ошибки setTimeout(() => { clearInterval(checkInterval); resolve('regular'); }, 5000); }); } pendingChecks.add(taskId); try { const response = await fetch(`/api/tasks/${taskId}/type`); if (!response.ok) { if (response.status === 404) { taskTypeCache.set(taskId, 'regular'); return 'regular'; } throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); const taskType = data.task_type || 'regular'; // Сохраняем в кэш taskTypeCache.set(taskId, taskType); return taskType; } catch (error) { console.error(`❌ Ошибка получения типа задачи ${taskId}:`, error); taskTypeCache.set(taskId, 'regular'); return 'regular'; } finally { pendingChecks.delete(taskId); } } // Проверка, раскрыта ли задача function isTaskExpanded(taskCard) { // Проверяем по различным селекторам for (const selector of EXPANDED_TASK_SELECTORS) { if (taskCard.matches(selector) || taskCard.querySelector(selector)) { return true; } } // Проверяем наличие классов раскрытия if (taskCard.classList.contains('expanded') || taskCard.classList.contains('open') || taskCard.classList.contains('active')) { return true; } // Проверяем атрибуты if (taskCard.getAttribute('data-expanded') === 'true' || taskCard.getAttribute('aria-expanded') === 'true') { return true; } return false; } // Функции для работы с календарем function formatDate(date) { if (!date) return ''; const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = date.getFullYear(); return `${day}.${month}.${year}`; } function parseDate(dateStr) { if (!dateStr) return null; const parts = dateStr.split('.'); if (parts.length === 3) { const day = parseInt(parts[0], 10); const month = parseInt(parts[1], 10) - 1; const year = parseInt(parts[2], 10); return new Date(year, month, day); } return null; } function getMonthName(month, year) { const date = new Date(year, month, 1); return date.toLocaleString('ru-RU', { month: 'long' }); } function generateCalendar(year, month, selectedDate) { const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); // Корректировка первого дня (0 - воскресенье, делаем понедельник первым) let startOffset = firstDay === 0 ? 6 : firstDay - 1; const today = new Date(); const todayStr = formatDate(today); let html = ''; let dayCount = 1; for (let i = 0; i < 6; i++) { for (let j = 0; j < 7; j++) { if (i === 0 && j < startOffset) { html += '
'; } else if (dayCount <= daysInMonth) { const currentDate = new Date(year, month, dayCount); const dateStr = formatDate(currentDate); const isSelected = selectedDate && dateStr === formatDate(selectedDate); const isToday = dateStr === todayStr; html += `
${dayCount}
`; dayCount++; } else { html += '
'; } } } return html; } function renderCalendar() { const calendar = document.getElementById('inlineCalendar'); if (!calendar) return; const year = calendarState.currentDate.getFullYear(); const month = calendarState.currentDate.getMonth(); const monthName = getMonthName(month, year); let html = `
${monthName} ${year}
Пн
Вт
Ср
Чт
Пт
Сб
Вс
${generateCalendar(year, month, calendarState.selectedDate)}
`; calendar.innerHTML = html; } function toggleCalendar() { if (calendarState.isOpen) { closeCalendar(); } else { openCalendar(); } } function openCalendar() { const calendar = document.getElementById('inlineCalendar'); if (!calendar) return; const dateInput = document.getElementById('documentDate'); if (dateInput && dateInput.value) { const parsedDate = parseDate(dateInput.value); if (parsedDate && !isNaN(parsedDate.getTime())) { calendarState.selectedDate = parsedDate; calendarState.currentDate = new Date(parsedDate); } } calendarState.isOpen = true; calendar.style.display = 'block'; renderCalendar(); } function closeCalendar() { const calendar = document.getElementById('inlineCalendar'); if (calendar) { calendar.style.display = 'none'; } calendarState.isOpen = false; } function selectDate(dateStr) { const dateInput = document.getElementById('documentDate'); if (dateInput) { dateInput.value = dateStr; calendarState.selectedDate = parseDate(dateStr); } closeCalendar(); } function prevMonth() { calendarState.currentDate.setMonth(calendarState.currentDate.getMonth() - 1); renderCalendar(); } function nextMonth() { calendarState.currentDate.setMonth(calendarState.currentDate.getMonth() + 1); renderCalendar(); } // Создание модального окна function createModal() { if (!document.getElementById('document-fields-styles')) { const styleElement = document.createElement('div'); styleElement.id = 'document-fields-styles'; styleElement.innerHTML = CONFIG.modalStyles + CONFIG.buttonStyles; document.head.appendChild(styleElement); } if (document.getElementById(CONFIG.modalId)) { return; } const modalHTML = `

📄 Реквизиты документа

`; document.body.insertAdjacentHTML('beforeend', modalHTML); // Добавляем обработчик клика вне календаря для его закрытия document.addEventListener('click', function(event) { const calendar = document.getElementById('inlineCalendar'); const calendarIcon = document.querySelector('.calendar-icon'); if (calendar && calendarState.isOpen) { if (!calendar.contains(event.target) && !calendarIcon.contains(event.target)) { closeCalendar(); } } }); } // Открытие модального окна async function openModal(taskId) { // Проверяем тип задачи const taskType = await getTaskType(taskId); if (taskType !== 'document') { alert('Эта функция доступна только для задач типа "Документ"'); return; } const modal = document.getElementById(CONFIG.modalId); if (!modal) { createModal(); } const modalElement = document.getElementById(CONFIG.modalId); const form = document.getElementById('documentFieldsForm'); const loading = document.getElementById('documentFieldsLoading'); const errorDiv = document.getElementById('documentFieldsError'); const messageDiv = document.getElementById('documentFieldsMessage'); if (errorDiv) errorDiv.style.display = 'none'; if (messageDiv) messageDiv.style.display = 'none'; if (form) form.style.display = 'none'; if (loading) loading.style.display = 'block'; modalElement.classList.add('active'); const taskIdSpan = document.getElementById('documentTaskId'); if (taskIdSpan) taskIdSpan.textContent = taskId; const authorInput = document.getElementById('documentAuthor'); if (authorInput && currentUser) { authorInput.value = currentUser.login || ''; } try { const response = await fetch(`/api/tasks/${taskId}/document-fields`); if (!response.ok) { throw new Error('Ошибка загрузки данных'); } const result = await response.json(); if (result.success) { const numberInput = document.getElementById('documentNumber'); const dateInput = document.getElementById('documentDate'); if (numberInput) numberInput.value = result.data.document_n || ''; if (dateInput) { dateInput.value = result.data.document_d || ''; // Устанавливаем выбранную дату в календарь if (result.data.document_d) { const parsedDate = parseDate(result.data.document_d); if (parsedDate && !isNaN(parsedDate.getTime())) { calendarState.selectedDate = parsedDate; calendarState.currentDate = new Date(parsedDate); } } else { calendarState.selectedDate = null; calendarState.currentDate = new Date(); } } if (result.data.document_a && authorInput && !authorInput.value) { authorInput.value = result.data.document_a; } } } catch (error) { console.error('❌ Ошибка загрузки полей документа:', error); showError('Ошибка загрузки данных: ' + error.message); } finally { if (loading) loading.style.display = 'none'; if (form) form.style.display = 'block'; } } // Закрытие модального окна function closeModal() { const modal = document.getElementById(CONFIG.modalId); if (modal) { modal.classList.remove('active'); const numberInput = document.getElementById('documentNumber'); const dateInput = document.getElementById('documentDate'); const authorInput = document.getElementById('documentAuthor'); if (numberInput) numberInput.value = ''; if (dateInput) dateInput.value = ''; if (authorInput) authorInput.value = ''; // Закрываем календарь closeCalendar(); // Сбрасываем состояние календаря calendarState = { currentDate: new Date(), selectedDate: null, isOpen: false, inputElement: null }; } } // Показать ошибку function showError(message) { const errorDiv = document.getElementById('documentFieldsError'); if (!errorDiv) return; errorDiv.textContent = message; errorDiv.style.display = 'block'; setTimeout(() => { errorDiv.style.display = 'none'; }, 5000); } // Показать успех function showSuccess(message) { const messageDiv = document.getElementById('documentFieldsMessage'); if (!messageDiv) return; messageDiv.textContent = message; messageDiv.style.display = 'block'; setTimeout(() => { messageDiv.style.display = 'none'; closeModal(); }, 2000); } // Валидация даты function validateDate(dateStr) { if (!dateStr) return true; const datePattern = /^(\d{2})\.(\d{2})\.(\d{4})$/; if (!datePattern.test(dateStr)) { return 'Неверный формат даты. Используйте ДД.ММ.ГГГГ'; } const [, day, month, year] = dateStr.match(datePattern); const date = new Date(year, month - 1, day); if (date.getDate() != day || date.getMonth() + 1 != month || date.getFullYear() != year) { return 'Некорректная дата'; } return true; } // Сохранение полей async function saveFields() { const taskId = document.getElementById('documentTaskId')?.textContent; if (!taskId) { showError('ID задачи не найден'); return; } const document_n = document.getElementById('documentNumber')?.value.trim() || ''; let document_d = document.getElementById('documentDate')?.value.trim() || ''; const document_a = document.getElementById('documentAuthor')?.value.trim() || currentUser?.login; if (document_d) { const dateValidation = validateDate(document_d); if (dateValidation !== true) { showError(dateValidation); return; } } const saveBtn = document.querySelector('.document-fields-btn.save'); if (!saveBtn) return; const originalText = saveBtn.textContent; saveBtn.textContent = 'Сохранение...'; saveBtn.disabled = true; try { const response = await fetch(`/api/tasks/${taskId}/document-fields`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ document_n: document_n || null, document_d: document_d || null, document_a: document_a || null }) }); const result = await response.json(); if (response.ok && result.success) { showSuccess('✅ Поля документа сохранены'); updateTaskCardDisplay(taskId, result.data); } else { showError(result.error || 'Ошибка сохранения'); } } catch (error) { console.error('❌ Ошибка сохранения:', error); showError('Ошибка сети: ' + error.message); } finally { saveBtn.textContent = originalText; saveBtn.disabled = false; } } // Обновление отображения в карточке задачи function updateTaskCardDisplay(taskId, data) { const taskCards = document.querySelectorAll(`[data-task-id="${taskId}"]`); taskCards.forEach(card => { let docContainer = card.querySelector('.document-fields-display'); if (!docContainer) { docContainer = document.createElement('div'); docContainer.className = 'document-fields-display'; const header = card.querySelector('.task-header'); if (header) { header.after(docContainer); } else { card.prepend(docContainer); } } let displayHTML = '
'; if (data.document_n) { displayHTML += `№: ${data.document_n}`; } if (data.document_d) { displayHTML += `Дата: ${data.document_d}`; } if (data.document_a) { displayHTML += `Автор: ${data.document_a}`; } if (!data.document_n && !data.document_d) { displayHTML += 'Нет данных документа'; } displayHTML += '
'; docContainer.innerHTML = displayHTML; }); } // Добавление кнопки в раскрытую задачу async function addButtonToExpandedTask(taskCard) { if (!currentUser) return; // Проверяем, имеет ли пользователь доступ (Подписант или Секретарь) if (!hasUserAccess()) { return; } const taskId = taskCard.dataset.taskId; if (!taskId) return; // Проверяем, есть ли уже кнопка if (taskCard.querySelector('.document-fields-btn-icon')) return; // Проверяем, раскрыта ли задача if (!isTaskExpanded(taskCard)) return; // Показываем индикатор загрузки const actionsContainer = taskCard.querySelector('.action-buttons, .task-actions'); if (!actionsContainer) return; const loadingIndicator = document.createElement('span'); loadingIndicator.className = 'document-fields-placeholder'; loadingIndicator.title = 'Проверка типа задачи...'; actionsContainer.appendChild(loadingIndicator); try { // Получаем тип задачи const taskType = await getTaskType(taskId); // Удаляем индикатор загрузки loadingIndicator.remove(); // Добавляем кнопку только для задач с типом "document" if (taskType === 'document') { const btn = document.createElement('button'); btn.className = 'document-fields-btn-icon document'; btn.innerHTML = '📄 Реквизиты'; btn.onclick = (e) => { e.stopPropagation(); window.documentFields.openModal(taskId); }; btn.title = 'Редактировать реквизиты документа'; actionsContainer.appendChild(btn); console.log(`✅ Добавлена кнопка для раскрытой задачи ${taskId} (тип: document)`); } } catch (error) { console.error(`❌ Ошибка проверки задачи ${taskId}:`, error); loadingIndicator.remove(); } } // Обработчик раскрытия задачи function handleTaskExpand(mutations) { for (const mutation of mutations) { if (mutation.type === 'attributes' && (mutation.attributeName === 'class' || mutation.attributeName === 'data-expanded')) { const target = mutation.target; if (target.dataset?.taskId && isTaskExpanded(target)) { addButtonToExpandedTask(target); } } if (mutation.type === 'childList' && mutation.addedNodes.length) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { // Element node // Проверяем сам элемент if (node.dataset?.taskId && isTaskExpanded(node)) { addButtonToExpandedTask(node); } // Проверяем дочерние элементы const expandedTasks = node.querySelectorAll('[data-task-id]'); expandedTasks.forEach(task => { if (isTaskExpanded(task)) { addButtonToExpandedTask(task); } }); } } } } } // Наблюдатель за изменениями DOM function observeDOM() { // Наблюдатель за атрибутами и появлением новых элементов const observer = new MutationObserver(handleTaskExpand); observer.observe(document.body, { attributes: true, attributeFilter: ['class', 'data-expanded', 'aria-expanded'], childList: true, subtree: true }); console.log('👀 Наблюдатель за раскрытием задач запущен'); // Также проверяем уже раскрытые задачи при загрузке setTimeout(() => { document.querySelectorAll('[data-task-id]').forEach(task => { if (isTaskExpanded(task)) { addButtonToExpandedTask(task); } }); }, 2000); } // Очистка кэша function clearCache() { taskTypeCache.clear(); pendingChecks.clear(); console.log('🗑️ Кэш типов задач очищен'); } // Функция для ручного обновления групп пользователя async function refreshUserGroups() { if (currentUser) { await getAllUserGroups(currentUser.login || currentUser.id); return hasUserAccess(); } return false; } // Инициализация async function init() { console.log('🔄 DocumentFields module initializing...'); try { await getCurrentUser(); // Проверяем доступ пользователя if (hasUserAccess()) { createModal(); observeDOM(); console.log('✅ DocumentFields module loaded (with access)'); // Для отладки показываем группы setTimeout(debugUserGroups, 1000); } else { console.log('ℹ️ DocumentFields module loaded (no access - user is not Signer or Secretary)'); debugUserGroups(); // Показываем отладку даже при отсутствии доступа } } catch (error) { console.error('❌ Ошибка инициализации:', error); } } // Экспортируем функции window.documentFields = { openModal, closeModal, saveFields, getTaskType, clearCache, init, addButtonToExpandedTask, isTaskExpanded, hasUserAccess, getAllUserGroups, refreshUserGroups, debugUserGroups, // Функции календаря toggleCalendar, selectDate, prevMonth, nextMonth }; // Запускаем инициализацию if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();