реквизиты

This commit is contained in:
2026-02-22 11:31:40 +05:00
parent 7e748c37e1
commit 2107d4ffc6
3 changed files with 725 additions and 0 deletions

605
public/document-fields.js Normal file
View File

@@ -0,0 +1,605 @@
// 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();
}
})();

View File

@@ -450,6 +450,7 @@
<script src="tasks_files.js"></script>
<script src="navbar.js"></script>
<script src="chat-ui.js"></script>
<script src="document-fields.js"></script>
</body>
</html>