реквизиты

This commit is contained in:
2026-02-22 11:41:13 +05:00
parent 2107d4ffc6
commit 01238e93d9
2 changed files with 311 additions and 72 deletions

View File

@@ -1,4 +1,5 @@
// document-fields.js - Скрипт для управления полями документа в задачах // document-fields.js - Скрипт для управления полями документа в задачах
// Показывает кнопку "Реквизиты" только для задач с типом "document" и только когда задача раскрыта
(function() { (function() {
'use strict'; 'use strict';
@@ -202,6 +203,9 @@
cursor: pointer; cursor: pointer;
font-size: 0.9em; font-size: 0.9em;
transition: background-color 0.2s; transition: background-color 0.2s;
display: inline-flex;
align-items: center;
gap: 4px;
} }
.document-fields-btn-icon:hover { .document-fields-btn-icon:hover {
@@ -216,6 +220,11 @@
background-color: #7B1FA2; background-color: #7B1FA2;
} }
.document-fields-btn-icon.document.loading {
opacity: 0.7;
cursor: wait;
}
.document-fields-badge { .document-fields-badge {
display: inline-block; display: inline-block;
padding: 2px 8px; padding: 2px 8px;
@@ -225,6 +234,22 @@
color: #1976d2; color: #1976d2;
margin-left: 8px; 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); }
}
</style> </style>
` `
}; };
@@ -232,14 +257,32 @@
// Текущий пользователь // Текущий пользователь
let currentUser = null; 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() { async function getCurrentUser() {
try { try {
const response = await fetch('/api/user'); const response = await fetch('/api/user');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json(); const data = await response.json();
if (data.user) { if (data.user) {
currentUser = data.user; currentUser = data.user;
console.log('✅ DocumentFields: текущий пользователь', currentUser); console.log('✅ DocumentFields: текущий пользователь', currentUser.login);
} }
return currentUser; return currentUser;
} catch (error) { } 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() { function createModal() {
// Добавляем стили
if (!document.getElementById('document-fields-styles')) { if (!document.getElementById('document-fields-styles')) {
const styleElement = document.createElement('div'); const styleElement = document.createElement('div');
styleElement.id = 'document-fields-styles'; styleElement.id = 'document-fields-styles';
@@ -258,7 +380,6 @@
document.head.appendChild(styleElement); document.head.appendChild(styleElement);
} }
// Проверяем, существует ли уже модальное окно
if (document.getElementById(CONFIG.modalId)) { if (document.getElementById(CONFIG.modalId)) {
return; return;
} }
@@ -313,6 +434,14 @@
// Открытие модального окна // Открытие модального окна
async function openModal(taskId) { async function openModal(taskId) {
// Проверяем тип задачи
const taskType = await getTaskType(taskId);
if (taskType !== 'document') {
alert('Эта функция доступна только для задач типа "Документ"');
return;
}
const modal = document.getElementById(CONFIG.modalId); const modal = document.getElementById(CONFIG.modalId);
if (!modal) { if (!modal) {
createModal(); createModal();
@@ -324,28 +453,23 @@
const errorDiv = document.getElementById('documentFieldsError'); const errorDiv = document.getElementById('documentFieldsError');
const messageDiv = document.getElementById('documentFieldsMessage'); const messageDiv = document.getElementById('documentFieldsMessage');
// Скрываем предыдущие сообщения if (errorDiv) errorDiv.style.display = 'none';
errorDiv.style.display = 'none'; if (messageDiv) messageDiv.style.display = 'none';
messageDiv.style.display = 'none';
// Показываем загрузку if (form) form.style.display = 'none';
form.style.display = 'none'; if (loading) loading.style.display = 'block';
loading.style.display = 'block';
// Активируем модальное окно
modalElement.classList.add('active'); modalElement.classList.add('active');
// Устанавливаем ID задачи const taskIdSpan = document.getElementById('documentTaskId');
document.getElementById('documentTaskId').textContent = taskId; if (taskIdSpan) taskIdSpan.textContent = taskId;
// Устанавливаем автора (логин текущего пользователя)
const authorInput = document.getElementById('documentAuthor'); const authorInput = document.getElementById('documentAuthor');
if (currentUser) { if (authorInput && currentUser) {
authorInput.value = currentUser.login || ''; authorInput.value = currentUser.login || '';
} }
try { try {
// Загружаем текущие значения
const response = await fetch(`/api/tasks/${taskId}/document-fields`); const response = await fetch(`/api/tasks/${taskId}/document-fields`);
if (!response.ok) { if (!response.ok) {
throw new Error('Ошибка загрузки данных'); throw new Error('Ошибка загрузки данных');
@@ -354,10 +478,13 @@
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
document.getElementById('documentNumber').value = result.data.document_n || ''; const numberInput = document.getElementById('documentNumber');
document.getElementById('documentDate').value = result.data.document_d || ''; const dateInput = document.getElementById('documentDate');
// Автор из БД может отличаться от текущего пользователя
if (result.data.document_a && !authorInput.value) { 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; authorInput.value = result.data.document_a;
} }
} }
@@ -365,9 +492,8 @@
console.error('❌ Ошибка загрузки полей документа:', error); console.error('❌ Ошибка загрузки полей документа:', error);
showError('Ошибка загрузки данных: ' + error.message); showError('Ошибка загрузки данных: ' + error.message);
} finally { } finally {
// Скрываем загрузку, показываем форму if (loading) loading.style.display = 'none';
loading.style.display = 'none'; if (form) form.style.display = 'block';
form.style.display = 'block';
} }
} }
@@ -376,12 +502,22 @@
const modal = document.getElementById(CONFIG.modalId); const modal = document.getElementById(CONFIG.modalId);
if (modal) { if (modal) {
modal.classList.remove('active'); 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) { function showError(message) {
const errorDiv = document.getElementById('documentFieldsError'); const errorDiv = document.getElementById('documentFieldsError');
if (!errorDiv) return;
errorDiv.textContent = message; errorDiv.textContent = message;
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
@@ -393,6 +529,8 @@
// Показать успех // Показать успех
function showSuccess(message) { function showSuccess(message) {
const messageDiv = document.getElementById('documentFieldsMessage'); const messageDiv = document.getElementById('documentFieldsMessage');
if (!messageDiv) return;
messageDiv.textContent = message; messageDiv.textContent = message;
messageDiv.style.display = 'block'; messageDiv.style.display = 'block';
@@ -404,9 +542,8 @@
// Валидация даты // Валидация даты
function validateDate(dateStr) { function validateDate(dateStr) {
if (!dateStr) return true; // Пустая дата допустима if (!dateStr) return true;
// Проверка формата ДД.ММ.ГГГГ
const datePattern = /^(\d{2})\.(\d{2})\.(\d{4})$/; const datePattern = /^(\d{2})\.(\d{2})\.(\d{4})$/;
if (!datePattern.test(dateStr)) { if (!datePattern.test(dateStr)) {
return 'Неверный формат даты. Используйте ДД.ММ.ГГГГ'; return 'Неверный формат даты. Используйте ДД.ММ.ГГГГ';
@@ -424,12 +561,16 @@
// Сохранение полей // Сохранение полей
async function saveFields() { async function saveFields() {
const taskId = document.getElementById('documentTaskId').textContent; const taskId = document.getElementById('documentTaskId')?.textContent;
const document_n = document.getElementById('documentNumber').value.trim(); if (!taskId) {
let document_d = document.getElementById('documentDate').value.trim(); showError('ID задачи не найден');
const document_a = document.getElementById('documentAuthor').value.trim() || currentUser?.login; 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) { if (document_d) {
const dateValidation = validateDate(document_d); const dateValidation = validateDate(document_d);
if (dateValidation !== true) { if (dateValidation !== true) {
@@ -438,8 +579,9 @@
} }
} }
// Показываем загрузку
const saveBtn = document.querySelector('.document-fields-btn.save'); const saveBtn = document.querySelector('.document-fields-btn.save');
if (!saveBtn) return;
const originalText = saveBtn.textContent; const originalText = saveBtn.textContent;
saveBtn.textContent = 'Сохранение...'; saveBtn.textContent = 'Сохранение...';
saveBtn.disabled = true; saveBtn.disabled = true;
@@ -461,8 +603,6 @@
if (response.ok && result.success) { if (response.ok && result.success) {
showSuccess('✅ Поля документа сохранены'); showSuccess('✅ Поля документа сохранены');
// Обновляем отображение в карточке задачи, если есть
updateTaskCardDisplay(taskId, result.data); updateTaskCardDisplay(taskId, result.data);
} else { } else {
showError(result.error || 'Ошибка сохранения'); showError(result.error || 'Ошибка сохранения');
@@ -481,14 +621,12 @@
const taskCards = document.querySelectorAll(`[data-task-id="${taskId}"]`); const taskCards = document.querySelectorAll(`[data-task-id="${taskId}"]`);
taskCards.forEach(card => { taskCards.forEach(card => {
// Ищем или создаем контейнер для полей документа
let docContainer = card.querySelector('.document-fields-display'); let docContainer = card.querySelector('.document-fields-display');
if (!docContainer) { if (!docContainer) {
docContainer = document.createElement('div'); docContainer = document.createElement('div');
docContainer.className = 'document-fields-display'; docContainer.className = 'document-fields-display';
// Вставляем после заголовка или в подходящее место
const header = card.querySelector('.task-header'); const header = card.querySelector('.task-header');
if (header) { if (header) {
header.after(docContainer); header.after(docContainer);
@@ -497,7 +635,6 @@
} }
} }
// Формируем HTML для отображения
let displayHTML = '<div style="margin: 8px 0; padding: 5px; background-color: #f5f5f5; border-radius: 4px; font-size: 0.9em;">'; let displayHTML = '<div style="margin: 8px 0; padding: 5px; background-color: #f5f5f5; border-radius: 4px; font-size: 0.9em;">';
if (data.document_n) { if (data.document_n) {
@@ -522,20 +659,37 @@
}); });
} }
// Добавление кнопки в карточки задач // Добавление кнопки в раскрытую задачу
function addDocumentFieldsButtons() { async function addButtonToExpandedTask(taskCard) {
if (!currentUser) return; if (!currentUser) return;
document.querySelectorAll('[data-task-id]').forEach(card => { const taskId = taskCard.dataset.taskId;
// Проверяем, есть ли уже кнопка if (!taskId) return;
if (card.querySelector('.document-fields-btn-icon')) return;
const taskId = card.dataset.taskId; // Проверяем, есть ли уже кнопка
if (taskCard.querySelector('.document-fields-btn-icon')) return;
// Ищем контейнер action-buttons // Проверяем, раскрыта ли задача
const actionsContainer = card.querySelector('.action-buttons, .task-actions'); if (!isTaskExpanded(taskCard)) return;
if (actionsContainer) { // Показываем индикатор загрузки
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'); const btn = document.createElement('button');
btn.className = 'document-fields-btn-icon document'; btn.className = 'document-fields-btn-icon document';
btn.innerHTML = '📄 Реквизиты'; btn.innerHTML = '📄 Реквизиты';
@@ -545,57 +699,107 @@
}; };
btn.title = 'Редактировать реквизиты документа'; btn.title = 'Редактировать реквизиты документа';
actionsContainer.appendChild(btn); 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 // Наблюдатель за изменениями DOM
function observeDOM() { function observeDOM() {
const observer = new MutationObserver((mutations) => { // Наблюдатель за атрибутами и появлением новых элементов
mutations.forEach((mutation) => { const observer = new MutationObserver(handleTaskExpand);
if (mutation.addedNodes.length) {
window.documentFields.addButtons();
}
});
});
observer.observe(document.body, { observer.observe(document.body, {
attributes: true,
attributeFilter: ['class', 'data-expanded', 'aria-expanded'],
childList: true, childList: true,
subtree: 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() { async function init() {
console.log('🔄 DocumentFields module initializing...'); console.log('🔄 DocumentFields module initializing...');
// Получаем текущего пользователя try {
await getCurrentUser(); await getCurrentUser();
createModal();
observeDOM();
// Создаем модальное окно console.log('✅ DocumentFields module loaded');
createModal(); } catch (error) {
console.error('❌ Ошибка инициализации:', error);
// Добавляем кнопки на существующие карточки }
setTimeout(() => {
window.documentFields.addButtons();
}, 1000);
// Запускаем наблюдение за DOM
observeDOM();
console.log('✅ DocumentFields module loaded');
} }
// Экспортируем функции в глобальную область // Экспортируем функции
window.documentFields = { window.documentFields = {
openModal, openModal,
closeModal, closeModal,
saveFields, saveFields,
addButtons: addDocumentFieldsButtons, getTaskType,
init clearCache,
init,
addButtonToExpandedTask,
isTaskExpanded
}; };
// Запускаем инициализацию после загрузки DOM // Запускаем инициализацию
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init); document.addEventListener('DOMContentLoaded', init);
} else { } else {

View File

@@ -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 }; module.exports = { setupTaskEndpoints,getApproverUsers };