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

605 lines
23 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 - Скрипт для управления полями документа в задачах
(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;
}
.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-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8em;
background-color: #e3f2fd;
color: #1976d2;
margin-left: 8px;
}
</style>
`
};
// Текущий пользователь
let currentUser = null;
// Получение текущего пользователя
async function getCurrentUser() {
try {
const response = await fetch('/api/user');
const data = await response.json();
if (data.user) {
currentUser = data.user;
console.log('✅ DocumentFields: текущий пользователь', currentUser);
}
return currentUser;
} catch (error) {
console.error('❌ DocumentFields: ошибка получения пользователя', error);
return null;
}
}
// Создание модального окна
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 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');
// Скрываем предыдущие сообщения
errorDiv.style.display = 'none';
messageDiv.style.display = 'none';
// Показываем загрузку
form.style.display = 'none';
loading.style.display = 'block';
// Активируем модальное окно
modalElement.classList.add('active');
// Устанавливаем ID задачи
document.getElementById('documentTaskId').textContent = taskId;
// Устанавливаем автора (логин текущего пользователя)
const authorInput = document.getElementById('documentAuthor');
if (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) {
document.getElementById('documentNumber').value = result.data.document_n || '';
document.getElementById('documentDate').value = result.data.document_d || '';
// Автор из БД может отличаться от текущего пользователя
if (result.data.document_a && !authorInput.value) {
authorInput.value = result.data.document_a;
}
}
} catch (error) {
console.error('❌ Ошибка загрузки полей документа:', error);
showError('Ошибка загрузки данных: ' + error.message);
} finally {
// Скрываем загрузку, показываем форму
loading.style.display = 'none';
form.style.display = 'block';
}
}
// Закрытие модального окна
function closeModal() {
const modal = document.getElementById(CONFIG.modalId);
if (modal) {
modal.classList.remove('active');
}
}
// Показать ошибку
function showError(message) {
const errorDiv = document.getElementById('documentFieldsError');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
setTimeout(() => {
errorDiv.style.display = 'none';
}, 5000);
}
// Показать успех
function showSuccess(message) {
const messageDiv = document.getElementById('documentFieldsMessage');
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;
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');
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);
}
}
// Формируем HTML для отображения
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;
});
}
// Добавление кнопки в карточки задач
function addDocumentFieldsButtons() {
if (!currentUser) return;
document.querySelectorAll('[data-task-id]').forEach(card => {
// Проверяем, есть ли уже кнопка
if (card.querySelector('.document-fields-btn-icon')) return;
const taskId = card.dataset.taskId;
// Ищем контейнер action-buttons
const actionsContainer = card.querySelector('.action-buttons, .task-actions');
if (actionsContainer) {
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);
}
});
}
// Наблюдатель за изменениями DOM
function observeDOM() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
window.documentFields.addButtons();
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
// Инициализация
async function init() {
console.log('🔄 DocumentFields module initializing...');
// Получаем текущего пользователя
await getCurrentUser();
// Создаем модальное окно
createModal();
// Добавляем кнопки на существующие карточки
setTimeout(() => {
window.documentFields.addButtons();
}, 1000);
// Запускаем наблюдение за DOM
observeDOM();
console.log('✅ DocumentFields module loaded');
}
// Экспортируем функции в глобальную область
window.documentFields = {
openModal,
closeModal,
saveFields,
addButtons: addDocumentFieldsButtons,
init
};
// Запускаем инициализацию после загрузки DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();