Files
minicrm/public/document-fields.js
2026-02-22 11:41:13 +05:00

809 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// document-fields.js - Скрипт для управления полями документа в задачах
// Показывает кнопку "Реквизиты" только для задач с типом "document" и только когда задача раскрыта
(function() {
'use strict';
// Конфигурация
const CONFIG = {
modalId: 'documentFieldsModal',
modalStyles: `
<style>
.document-fields-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
z-index: 10000;
justify-content: center;
align-items: center;
}
.document-fields-modal.active {
display: flex;
}
.document-fields-content {
background: white;
padding: 25px;
border-radius: 8px;
max-width: 500px;
width: 90%;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.document-fields-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
.document-fields-header h3 {
margin: 0;
color: #333;
font-size: 1.3em;
}
.document-fields-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0 5px;
}
.document-fields-close:hover {
color: #333;
}
.document-fields-form {
margin-bottom: 20px;
}
.document-field-group {
margin-bottom: 15px;
}
.document-field-group label {
display: block;
margin-bottom: 5px;
color: #555;
font-weight: 600;
font-size: 0.9em;
}
.document-field-group input {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1em;
box-sizing: border-box;
}
.document-field-group input:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 2px rgba(76,175,80,0.1);
}
.document-field-group input[readonly] {
background-color: #f9f9f9;
cursor: not-allowed;
}
.document-fields-buttons {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #f0f0f0;
}
.document-fields-btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 0.95em;
cursor: pointer;
transition: all 0.2s;
}
.document-fields-btn.save {
background-color: #4CAF50;
color: white;
}
.document-fields-btn.save:hover {
background-color: #45a049;
transform: translateY(-1px);
}
.document-fields-btn.cancel {
background-color: #f44336;
color: white;
}
.document-fields-btn.cancel:hover {
background-color: #da190b;
transform: translateY(-1px);
}
.document-fields-btn:active {
transform: translateY(0);
}
.document-fields-loading {
text-align: center;
padding: 30px;
color: #666;
}
.document-fields-error {
background-color: #ffebee;
color: #c62828;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
border: 1px solid #ef9a9a;
}
.document-fields-success {
background-color: #e8f5e9;
color: #2e7d32;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
border: 1px solid #a5d6a7;
}
.document-fields-info {
font-size: 0.9em;
color: #666;
margin-bottom: 10px;
padding: 8px;
background-color: #f5f5f5;
border-radius: 4px;
}
.document-fields-info strong {
color: #333;
}
</style>
`,
buttonStyles: `
<style>
.document-fields-btn-icon {
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
margin: 0 5px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s;
display: inline-flex;
align-items: center;
gap: 4px;
}
.document-fields-btn-icon:hover {
background-color: #1976D2;
}
.document-fields-btn-icon.document {
background-color: #9C27B0;
}
.document-fields-btn-icon.document:hover {
background-color: #7B1FA2;
}
.document-fields-btn-icon.document.loading {
opacity: 0.7;
cursor: wait;
}
.document-fields-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8em;
background-color: #e3f2fd;
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); }
}
</style>
`
};
// Текущий пользователь
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.login);
}
return currentUser;
} catch (error) {
console.error('❌ DocumentFields: ошибка получения пользователя', error);
return null;
}
}
// Получение типа задачи по 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';
styleElement.innerHTML = CONFIG.modalStyles + CONFIG.buttonStyles;
document.head.appendChild(styleElement);
}
if (document.getElementById(CONFIG.modalId)) {
return;
}
const modalHTML = `
<div id="${CONFIG.modalId}" class="document-fields-modal">
<div class="document-fields-content">
<div class="document-fields-header">
<h3>📄 Реквизиты документа</h3>
<button class="document-fields-close" onclick="documentFields.closeModal()">&times;</button>
</div>
<div id="documentFieldsMessage" class="document-fields-success" style="display: none;"></div>
<div id="documentFieldsError" class="document-fields-error" style="display: none;"></div>
<div id="documentFieldsLoading" class="document-fields-loading" style="display: none;">
Загрузка...
</div>
<div id="documentFieldsForm" class="document-fields-form" style="display: none;">
<div class="document-fields-info">
<strong>ID задачи:</strong> <span id="documentTaskId"></span>
</div>
<div class="document-field-group">
<label for="documentNumber">Номер документа:</label>
<input type="text" id="documentNumber" placeholder="Введите номер документа" maxlength="100">
</div>
<div class="document-field-group">
<label for="documentDate">Дата документа:</label>
<input type="text" id="documentDate" placeholder="ДД.ММ.ГГГГ" maxlength="10">
</div>
<div class="document-field-group">
<label for="documentAuthor">Автор (логин):</label>
<input type="text" id="documentAuthor" placeholder="Логин автора" readonly>
<small style="color: #666;">Автоматически устанавливается ваш логин</small>
</div>
<div class="document-fields-buttons">
<button class="document-fields-btn cancel" onclick="documentFields.closeModal()">Отмена</button>
<button class="document-fields-btn save" onclick="documentFields.saveFields()">Сохранить</button>
</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
// Открытие модального окна
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_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 = '';
}
}
// Показать ошибку
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 = '<div style="margin: 8px 0; padding: 5px; background-color: #f5f5f5; border-radius: 4px; font-size: 0.9em;">';
if (data.document_n) {
displayHTML += `<span style="margin-right: 15px;"><strong>№:</strong> ${data.document_n}</span>`;
}
if (data.document_d) {
displayHTML += `<span style="margin-right: 15px;"><strong>Дата:</strong> ${data.document_d}</span>`;
}
if (data.document_a) {
displayHTML += `<span><strong>Автор:</strong> ${data.document_a}</span>`;
}
if (!data.document_n && !data.document_d) {
displayHTML += '<span style="color: #999;">Нет данных документа</span>';
}
displayHTML += '</div>';
docContainer.innerHTML = displayHTML;
});
}
// Добавление кнопки в раскрытую задачу
async function addButtonToExpandedTask(taskCard) {
if (!currentUser) 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 init() {
console.log('🔄 DocumentFields module initializing...');
try {
await getCurrentUser();
createModal();
observeDOM();
console.log('✅ DocumentFields module loaded');
} catch (error) {
console.error('❌ Ошибка инициализации:', error);
}
}
// Экспортируем функции
window.documentFields = {
openModal,
closeModal,
saveFields,
getTaskType,
clearCache,
init,
addButtonToExpandedTask,
isTaskExpanded
};
// Запускаем инициализацию
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();