From 01238e93d90b5b37d7b0573015ecb2b16be356d3 Mon Sep 17 00:00:00 2001 From: kalugin66 Date: Sun, 22 Feb 2026 11:41:13 +0500 Subject: [PATCH] =?UTF-8?q?=D1=80=D0=B5=D0=BA=D0=B2=D0=B8=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/document-fields.js | 348 ++++++++++++++++++++++++++++++-------- task-endpoints.js | 35 ++++ 2 files changed, 311 insertions(+), 72 deletions(-) diff --git a/public/document-fields.js b/public/document-fields.js index ffd8652..c04866a 100644 --- a/public/document-fields.js +++ b/public/document-fields.js @@ -1,4 +1,5 @@ // document-fields.js - Скрипт для управления полями документа в задачах +// Показывает кнопку "Реквизиты" только для задач с типом "document" и только когда задача раскрыта (function() { 'use strict'; @@ -202,6 +203,9 @@ cursor: pointer; font-size: 0.9em; transition: background-color 0.2s; + display: inline-flex; + align-items: center; + gap: 4px; } .document-fields-btn-icon:hover { @@ -216,6 +220,11 @@ background-color: #7B1FA2; } + .document-fields-btn-icon.document.loading { + opacity: 0.7; + cursor: wait; + } + .document-fields-badge { display: inline-block; padding: 2px 8px; @@ -225,21 +234,55 @@ color: #1976d2; margin-left: 8px; } + + .document-fields-placeholder { + display: inline-block; + width: 16px; + height: 16px; + border: 2px solid #f3f3f3; + border-top: 2px solid #9C27B0; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-right: 4px; + } + + @keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } ` }; // Текущий пользователь let currentUser = null; + + // Кэш для типов задач + 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' + ]; // Получение текущего пользователя 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); + console.log('✅ DocumentFields: текущий пользователь', currentUser.login); } return currentUser; } catch (error) { @@ -248,9 +291,88 @@ } } + // Получение типа задачи по 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 createModal() { - // Добавляем стили if (!document.getElementById('document-fields-styles')) { const styleElement = document.createElement('div'); styleElement.id = 'document-fields-styles'; @@ -258,7 +380,6 @@ document.head.appendChild(styleElement); } - // Проверяем, существует ли уже модальное окно if (document.getElementById(CONFIG.modalId)) { return; } @@ -313,6 +434,14 @@ // Открытие модального окна async function openModal(taskId) { + // Проверяем тип задачи + const taskType = await getTaskType(taskId); + + if (taskType !== 'document') { + alert('Эта функция доступна только для задач типа "Документ"'); + return; + } + const modal = document.getElementById(CONFIG.modalId); if (!modal) { createModal(); @@ -324,28 +453,23 @@ const errorDiv = document.getElementById('documentFieldsError'); const messageDiv = document.getElementById('documentFieldsMessage'); - // Скрываем предыдущие сообщения - errorDiv.style.display = 'none'; - messageDiv.style.display = 'none'; + if (errorDiv) errorDiv.style.display = 'none'; + if (messageDiv) messageDiv.style.display = 'none'; - // Показываем загрузку - form.style.display = 'none'; - loading.style.display = 'block'; + if (form) form.style.display = 'none'; + if (loading) loading.style.display = 'block'; - // Активируем модальное окно modalElement.classList.add('active'); - // Устанавливаем ID задачи - document.getElementById('documentTaskId').textContent = taskId; + const taskIdSpan = document.getElementById('documentTaskId'); + if (taskIdSpan) taskIdSpan.textContent = taskId; - // Устанавливаем автора (логин текущего пользователя) const authorInput = document.getElementById('documentAuthor'); - if (currentUser) { + if (authorInput && currentUser) { authorInput.value = currentUser.login || ''; } try { - // Загружаем текущие значения const response = await fetch(`/api/tasks/${taskId}/document-fields`); if (!response.ok) { throw new Error('Ошибка загрузки данных'); @@ -354,10 +478,13 @@ const result = await response.json(); if (result.success) { - document.getElementById('documentNumber').value = result.data.document_n || ''; - document.getElementById('documentDate').value = result.data.document_d || ''; - // Автор из БД может отличаться от текущего пользователя - if (result.data.document_a && !authorInput.value) { + 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_a && authorInput && !authorInput.value) { authorInput.value = result.data.document_a; } } @@ -365,9 +492,8 @@ console.error('❌ Ошибка загрузки полей документа:', error); showError('Ошибка загрузки данных: ' + error.message); } finally { - // Скрываем загрузку, показываем форму - loading.style.display = 'none'; - form.style.display = 'block'; + if (loading) loading.style.display = 'none'; + if (form) form.style.display = 'block'; } } @@ -376,12 +502,22 @@ 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 = ''; } } // Показать ошибку function showError(message) { const errorDiv = document.getElementById('documentFieldsError'); + if (!errorDiv) return; + errorDiv.textContent = message; errorDiv.style.display = 'block'; @@ -393,6 +529,8 @@ // Показать успех function showSuccess(message) { const messageDiv = document.getElementById('documentFieldsMessage'); + if (!messageDiv) return; + messageDiv.textContent = message; messageDiv.style.display = 'block'; @@ -404,9 +542,8 @@ // Валидация даты function validateDate(dateStr) { - if (!dateStr) return true; // Пустая дата допустима + if (!dateStr) return true; - // Проверка формата ДД.ММ.ГГГГ const datePattern = /^(\d{2})\.(\d{2})\.(\d{4})$/; if (!datePattern.test(dateStr)) { return 'Неверный формат даты. Используйте ДД.ММ.ГГГГ'; @@ -424,12 +561,16 @@ // Сохранение полей async function saveFields() { - const taskId = document.getElementById('documentTaskId').textContent; - 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; + 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) { @@ -438,8 +579,9 @@ } } - // Показываем загрузку const saveBtn = document.querySelector('.document-fields-btn.save'); + if (!saveBtn) return; + const originalText = saveBtn.textContent; saveBtn.textContent = 'Сохранение...'; saveBtn.disabled = true; @@ -461,8 +603,6 @@ if (response.ok && result.success) { showSuccess('✅ Поля документа сохранены'); - - // Обновляем отображение в карточке задачи, если есть updateTaskCardDisplay(taskId, result.data); } else { showError(result.error || 'Ошибка сохранения'); @@ -481,14 +621,12 @@ 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); @@ -497,7 +635,6 @@ } } - // Формируем HTML для отображения let displayHTML = '
'; if (data.document_n) { @@ -522,20 +659,37 @@ }); } - // Добавление кнопки в карточки задач - function addDocumentFieldsButtons() { + // Добавление кнопки в раскрытую задачу + async function addButtonToExpandedTask(taskCard) { if (!currentUser) return; - document.querySelectorAll('[data-task-id]').forEach(card => { - // Проверяем, есть ли уже кнопка - if (card.querySelector('.document-fields-btn-icon')) 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); - const taskId = card.dataset.taskId; + // Удаляем индикатор загрузки + loadingIndicator.remove(); - // Ищем контейнер action-buttons - const actionsContainer = card.querySelector('.action-buttons, .task-actions'); - - if (actionsContainer) { + // Добавляем кнопку только для задач с типом "document" + if (taskType === 'document') { const btn = document.createElement('button'); btn.className = 'document-fields-btn-icon document'; btn.innerHTML = '📄 Реквизиты'; @@ -545,57 +699,107 @@ }; 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((mutations) => { - mutations.forEach((mutation) => { - if (mutation.addedNodes.length) { - window.documentFields.addButtons(); - } - }); - }); + // Наблюдатель за атрибутами и появлением новых элементов + 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 init() { console.log('🔄 DocumentFields module initializing...'); - // Получаем текущего пользователя - await getCurrentUser(); - - // Создаем модальное окно - createModal(); - - // Добавляем кнопки на существующие карточки - setTimeout(() => { - window.documentFields.addButtons(); - }, 1000); - - // Запускаем наблюдение за DOM - observeDOM(); - - console.log('✅ DocumentFields module loaded'); + try { + await getCurrentUser(); + createModal(); + observeDOM(); + + console.log('✅ DocumentFields module loaded'); + } catch (error) { + console.error('❌ Ошибка инициализации:', error); + } } - // Экспортируем функции в глобальную область + // Экспортируем функции window.documentFields = { openModal, closeModal, saveFields, - addButtons: addDocumentFieldsButtons, - init + getTaskType, + clearCache, + init, + addButtonToExpandedTask, + isTaskExpanded }; - // Запускаем инициализацию после загрузки DOM + // Запускаем инициализацию if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { diff --git a/task-endpoints.js b/task-endpoints.js index 83cb9fc..8246d09 100644 --- a/task-endpoints.js +++ b/task-endpoints.js @@ -1934,6 +1934,41 @@ app.get('/api/tasks/:taskId/document-fields', requireAuth, (req, res) => { ); }); }); +// API для получения типа задачи +app.get('/api/tasks/:taskId/type', requireAuth, (req, res) => { + const { taskId } = req.params; + const userId = req.session.user.id; + + // Проверяем доступ к задаче + const { checkTaskAccess } = require('./database'); + + checkTaskAccess(userId, taskId, (err, hasAccess) => { + if (err || !hasAccess) { + return res.status(404).json({ error: 'Задача не найдена или у вас нет прав доступа' }); + } + + db.get( + "SELECT task_type FROM tasks WHERE id = ?", + [taskId], + (err, row) => { + if (err) { + console.error('❌ Ошибка получения типа задачи:', err); + return res.status(500).json({ error: err.message }); + } + + if (!row) { + return res.status(404).json({ error: 'Задача не найдена' }); + } + + res.json({ + success: true, + task_type: row.task_type || 'regular', + taskId: taskId + }); + } + ); + }); +}); } module.exports = { setupTaskEndpoints,getApproverUsers }; \ No newline at end of file