568 lines
24 KiB
JavaScript
568 lines
24 KiB
JavaScript
// doc.js - Согласование документов
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
if (window.location.pathname === '/doc') {
|
||
loadDocumentTypes();
|
||
setupDocumentForm();
|
||
loadMyDocuments();
|
||
setupDocumentFilters();
|
||
}
|
||
});
|
||
|
||
let documentTypes = [];
|
||
let allDocuments = [];
|
||
let filteredDocuments = [];
|
||
|
||
async function loadDocumentTypes() {
|
||
try {
|
||
const response = await fetch('/api/document-types');
|
||
documentTypes = await response.json();
|
||
populateDocumentTypeSelect();
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки типов документов:', error);
|
||
}
|
||
}
|
||
|
||
function populateDocumentTypeSelect() {
|
||
const select = document.getElementById('document-type');
|
||
if (!select) return;
|
||
|
||
select.innerHTML = '<option value="">Выберите тип документа</option>';
|
||
documentTypes.forEach(type => {
|
||
const option = document.createElement('option');
|
||
option.value = type.id;
|
||
option.textContent = type.name;
|
||
select.appendChild(option);
|
||
});
|
||
}
|
||
|
||
function setupDocumentForm() {
|
||
const form = document.getElementById('create-document-form');
|
||
if (!form) return;
|
||
|
||
form.addEventListener('submit', createDocument);
|
||
|
||
// Устанавливаем текущую дату для даты документа
|
||
const today = new Date().toISOString().split('T')[0];
|
||
const documentDateInput = document.getElementById('document-date');
|
||
if (documentDateInput) {
|
||
documentDateInput.value = today;
|
||
}
|
||
|
||
// Устанавливаем дату выполнения (по умолчанию через 7 дней)
|
||
const dueDate = new Date();
|
||
dueDate.setDate(dueDate.getDate() + 7);
|
||
const dueDateInput = document.getElementById('due-date');
|
||
if (dueDateInput) {
|
||
dueDateInput.value = dueDate.toISOString().split('T')[0];
|
||
}
|
||
}
|
||
|
||
async function createDocument(event) {
|
||
event.preventDefault();
|
||
|
||
if (!currentUser) {
|
||
alert('Требуется аутентификация');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
|
||
// Собираем данные формы
|
||
const title = document.getElementById('title').value;
|
||
const description = document.getElementById('description').value;
|
||
const documentTypeId = document.getElementById('document-type').value;
|
||
const documentNumber = document.getElementById('document-number').value;
|
||
const documentDate = document.getElementById('document-date').value;
|
||
const pagesCount = document.getElementById('pages-count').value;
|
||
const urgencyLevel = document.getElementById('urgency-level').value;
|
||
const dueDate = document.getElementById('due-date').value;
|
||
const comment = document.getElementById('comment').value;
|
||
|
||
if (!title || title.trim() === '') {
|
||
alert('Название документа обязательно');
|
||
return;
|
||
}
|
||
|
||
formData.append('title', title);
|
||
formData.append('description', description || '');
|
||
formData.append('dueDate', dueDate || '');
|
||
formData.append('documentTypeId', documentTypeId || '');
|
||
formData.append('documentNumber', documentNumber || '');
|
||
formData.append('documentDate', documentDate || '');
|
||
formData.append('pagesCount', pagesCount || '');
|
||
formData.append('urgencyLevel', urgencyLevel || 'normal');
|
||
formData.append('comment', comment || '');
|
||
|
||
// Добавляем файлы
|
||
const files = document.getElementById('files').files;
|
||
for (let i = 0; i < files.length; i++) {
|
||
formData.append('files', files[i]);
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/documents', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
alert(result.message || 'Документ успешно создан и отправлен на согласование!');
|
||
|
||
// Сбрасываем форму
|
||
document.getElementById('create-document-form').reset();
|
||
document.getElementById('file-list').innerHTML = '';
|
||
|
||
// Загружаем мои документы
|
||
loadMyDocuments();
|
||
|
||
// Возвращаемся к списку документов
|
||
showDocumentSection('my-documents');
|
||
} else {
|
||
const error = await response.json();
|
||
alert(error.error || 'Ошибка создания документа');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка:', error);
|
||
alert('Ошибка создания документа');
|
||
}
|
||
}
|
||
|
||
async function loadMyDocuments() {
|
||
try {
|
||
const response = await fetch('/api/documents/my');
|
||
allDocuments = await response.json();
|
||
filteredDocuments = [...allDocuments];
|
||
renderMyDocuments();
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки документов:', error);
|
||
document.getElementById('my-documents-list').innerHTML =
|
||
'<div class="loading">Ошибка загрузки документов</div>';
|
||
}
|
||
}
|
||
|
||
async function loadSecretaryDocuments() {
|
||
try {
|
||
const response = await fetch('/api/documents/secretary');
|
||
allDocuments = await response.json();
|
||
filteredDocuments = [...allDocuments];
|
||
renderSecretaryDocuments();
|
||
} catch (error) {
|
||
console.error('Ошибка загрузки документов секретаря:', error);
|
||
document.getElementById('secretary-documents-list').innerHTML =
|
||
'<div class="loading">Ошибка загрузки документов</div>';
|
||
}
|
||
}
|
||
|
||
function renderMyDocuments() {
|
||
const container = document.getElementById('my-documents-list');
|
||
|
||
if (filteredDocuments.length === 0) {
|
||
container.innerHTML = '<div class="loading">Нет документов</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = filteredDocuments.map(doc => {
|
||
const status = getDocumentStatus(doc);
|
||
const statusClass = getDocumentStatusClass(status);
|
||
const isCancelled = doc.status === 'cancelled';
|
||
const isClosed = doc.closed_at !== null;
|
||
|
||
const timeLeftInfo = getDocumentTimeLeftInfo(doc);
|
||
|
||
return `
|
||
<div class="document-card ${isCancelled ? 'cancelled' : ''} ${isClosed ? 'closed' : ''}">
|
||
<div class="document-header">
|
||
<div class="document-title">
|
||
<span class="document-number">Док. №${doc.document_number || doc.id}</span>
|
||
<strong>${doc.title}</strong>
|
||
${isCancelled ? '<span class="status-badge status-cancelled">Отозван</span>' : ''}
|
||
${isClosed ? '<span class="status-badge status-closed">Завершен</span>' : ''}
|
||
${timeLeftInfo ? `<span class="deadline-badge ${timeLeftInfo.class}">${timeLeftInfo.text}</span>` : ''}
|
||
<span class="status-badge ${statusClass}">${status}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="document-content">
|
||
<div class="document-actions">
|
||
${!isCancelled && !isClosed ? `
|
||
<button class="cancel-btn" onclick="cancelDocument(${doc.document_id})" title="Отозвать документ">🗑️</button>
|
||
` : ''}
|
||
${doc.files && doc.files.length > 0 ? `
|
||
<button class="download-btn" onclick="downloadDocumentPackage(${doc.document_id})" title="Скачать пакет документов">📦</button>
|
||
` : ''}
|
||
</div>
|
||
|
||
<div class="document-details">
|
||
<div><strong>Тип документа:</strong> ${doc.document_type_name || 'Не указан'}</div>
|
||
${doc.description ? `<div><strong>Описание:</strong> ${doc.description}</div>` : ''}
|
||
<div><strong>Статус согласования:</strong> ${doc.assignment_status || 'Не назначен'}</div>
|
||
${doc.refusal_reason ? `<div class="refusal-reason"><strong>Причина отказа:</strong> ${doc.refusal_reason}</div>` : ''}
|
||
${doc.comment ? `<div><strong>Комментарий создателя:</strong> ${doc.comment}</div>` : ''}
|
||
</div>
|
||
|
||
<div class="document-meta">
|
||
<div><strong>Дата документа:</strong> ${formatDate(doc.document_date)}</div>
|
||
<div><strong>Количество страниц:</strong> ${doc.pages_count || 'Не указано'}</div>
|
||
<div><strong>Срочность:</strong> ${getUrgencyText(doc.urgency_level)}</div>
|
||
<div><strong>Срок согласования:</strong> ${formatDate(doc.due_date)}</div>
|
||
</div>
|
||
|
||
<div class="document-files" id="files-${doc.id}">
|
||
<strong>Файлы:</strong>
|
||
${doc.files && doc.files.length > 0 ?
|
||
renderDocumentFiles(doc.files) :
|
||
'<span class="no-files">нет файлов</span>'
|
||
}
|
||
</div>
|
||
|
||
<div class="document-timeline">
|
||
<small>Создан: ${formatDateTime(doc.created_at)}</small>
|
||
${doc.closed_at ? `<br><small>Завершен: ${formatDateTime(doc.closed_at)}</small>` : ''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderSecretaryDocuments() {
|
||
const container = document.getElementById('secretary-documents-list');
|
||
|
||
if (filteredDocuments.length === 0) {
|
||
container.innerHTML = '<div class="loading">Нет документов для согласования</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = filteredDocuments.map(doc => {
|
||
const status = getDocumentStatus(doc);
|
||
const statusClass = getDocumentStatusClass(status);
|
||
|
||
const timeLeftInfo = getDocumentTimeLeftInfo(doc);
|
||
|
||
return `
|
||
<div class="document-card secretary">
|
||
<div class="document-header">
|
||
<div class="document-title">
|
||
<span class="document-number">Док. №${doc.document_number || doc.id}</span>
|
||
<strong>${doc.title}</strong>
|
||
${timeLeftInfo ? `<span class="deadline-badge ${timeLeftInfo.class}">${timeLeftInfo.text}</span>` : ''}
|
||
<span class="status-badge ${statusClass}">${status}</span>
|
||
<span class="creator-badge">От: ${doc.creator_name}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="document-content">
|
||
<div class="document-actions">
|
||
<button class="approve-btn" onclick="openApproveModal(${doc.document_id})" title="Согласовать">✅</button>
|
||
<button class="pre-approve-btn" onclick="openPreApproveModal(${doc.document_id})" title="Предварительно согласовать">📝</button>
|
||
<button class="refuse-btn" onclick="openRefuseModal(${doc.document_id})" title="Отказать">❌</button>
|
||
${doc.files && doc.files.length > 0 ? `
|
||
<button class="download-btn" onclick="downloadDocumentPackage(${doc.document_id})" title="Скачать пакет документов">📦</button>
|
||
` : ''}
|
||
</div>
|
||
|
||
<div class="document-details">
|
||
<div><strong>Тип документа:</strong> ${doc.document_type_name || 'Не указан'}</div>
|
||
${doc.description ? `<div><strong>Описание:</strong> ${doc.description}</div>` : ''}
|
||
${doc.comment ? `<div><strong>Комментарий создателя:</strong> ${doc.comment}</div>` : ''}
|
||
${doc.refusal_reason ? `<div class="refusal-reason"><strong>Ранее отказано:</strong> ${doc.refusal_reason}</div>` : ''}
|
||
</div>
|
||
|
||
<div class="document-meta">
|
||
<div><strong>Дата документа:</strong> ${formatDate(doc.document_date)}</div>
|
||
<div><strong>Номер документа:</strong> ${doc.document_number || 'Не указан'}</div>
|
||
<div><strong>Количество страниц:</strong> ${doc.pages_count || 'Не указано'}</div>
|
||
<div><strong>Срочность:</strong> ${getUrgencyText(doc.urgency_level)}</div>
|
||
<div><strong>Срок согласования:</strong> ${formatDate(doc.due_date)}</div>
|
||
</div>
|
||
|
||
<div class="document-files" id="files-${doc.id}">
|
||
<strong>Файлы:</strong>
|
||
${doc.files && doc.files.length > 0 ?
|
||
renderDocumentFiles(doc.files) :
|
||
'<span class="no-files">нет файлов</span>'
|
||
}
|
||
</div>
|
||
|
||
<div class="document-timeline">
|
||
<small>Создан: ${formatDateTime(doc.created_at)}</small>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderDocumentFiles(files) {
|
||
return `
|
||
<div class="file-icons-container">
|
||
${files.map(file => `
|
||
<div class="file-icon" onclick="downloadFile(${file.id})" title="${file.original_name} (${formatFileSize(file.file_size)})">
|
||
<i class="fas fa-file"></i>
|
||
<span>${file.original_name}</span>
|
||
<small>${formatFileSize(file.file_size)}</small>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function getDocumentStatus(doc) {
|
||
if (doc.status === 'cancelled') return 'Отозван';
|
||
if (doc.closed_at) return 'Завершен';
|
||
|
||
switch(doc.assignment_status) {
|
||
case 'pre_approved': return 'Предварительно согласован';
|
||
case 'approved': return 'Согласован';
|
||
case 'refused': return 'Отказано';
|
||
case 'received': return 'Получен оригинал';
|
||
case 'signed': return 'Подписан';
|
||
case 'assigned': return 'На согласовании';
|
||
default: return 'Создан';
|
||
}
|
||
}
|
||
|
||
function getDocumentStatusClass(status) {
|
||
switch(status) {
|
||
case 'Согласован':
|
||
case 'Подписан':
|
||
case 'Получен оригинал': return 'status-approved';
|
||
case 'Предварительно согласован': return 'status-pre-approved';
|
||
case 'Отказано': return 'status-refused';
|
||
case 'Отозван': return 'status-cancelled';
|
||
case 'Завершен': return 'status-closed';
|
||
default: return 'status-pending';
|
||
}
|
||
}
|
||
|
||
function getUrgencyText(urgency) {
|
||
switch(urgency) {
|
||
case 'very_urgent': return 'Очень срочно';
|
||
case 'urgent': return 'Срочно';
|
||
default: return 'Обычная';
|
||
}
|
||
}
|
||
|
||
function getDocumentTimeLeftInfo(doc) {
|
||
if (!doc.due_date || doc.closed_at) return null;
|
||
|
||
const dueDate = new Date(doc.due_date);
|
||
const now = new Date();
|
||
const timeLeft = dueDate.getTime() - now.getTime();
|
||
const daysLeft = Math.floor(timeLeft / (24 * 60 * 60 * 1000));
|
||
|
||
if (daysLeft <= 0) return null;
|
||
|
||
if (daysLeft <= 1) {
|
||
return {
|
||
text: `Менее 1 дня`,
|
||
class: 'deadline-urgent'
|
||
};
|
||
} else if (daysLeft <= 3) {
|
||
return {
|
||
text: `${daysLeft} дня`,
|
||
class: 'deadline-warning'
|
||
};
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
function formatDate(dateString) {
|
||
if (!dateString) return 'Не указана';
|
||
const date = new Date(dateString);
|
||
return date.toLocaleDateString('ru-RU');
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
if (bytes === 0) return '0 Bytes';
|
||
const k = 1024;
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
}
|
||
|
||
function downloadFile(fileId) {
|
||
window.open(`/api/files/${fileId}/download`, '_blank');
|
||
}
|
||
|
||
async function downloadDocumentPackage(documentId) {
|
||
try {
|
||
const response = await fetch(`/api/documents/${documentId}/package`);
|
||
const result = await response.json();
|
||
|
||
if (result.success && result.downloadUrl) {
|
||
window.open(result.downloadUrl, '_blank');
|
||
} else {
|
||
alert(result.message || 'Функция создания пакета документов будет реализована в следующей версии');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка скачивания пакета:', error);
|
||
alert('Ошибка скачивания пакета документов');
|
||
}
|
||
}
|
||
|
||
async function cancelDocument(documentId) {
|
||
if (!confirm('Вы уверены, что хотите отозвать этот документ?')) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/documents/${documentId}/cancel`, {
|
||
method: 'POST'
|
||
});
|
||
|
||
if (response.ok) {
|
||
alert('Документ отозван!');
|
||
loadMyDocuments();
|
||
} else {
|
||
const error = await response.json();
|
||
alert(error.error || 'Ошибка отзыва документа');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка:', error);
|
||
alert('Ошибка отзыва документа');
|
||
}
|
||
}
|
||
|
||
function openPreApproveModal(documentId) {
|
||
document.getElementById('approve-modal-type').value = 'pre_approve';
|
||
document.getElementById('approve-modal-document-id').value = documentId;
|
||
document.getElementById('approve-comment').value = '';
|
||
document.getElementById('refusal-reason').style.display = 'none';
|
||
document.getElementById('approve-modal').style.display = 'block';
|
||
}
|
||
|
||
function openApproveModal(documentId) {
|
||
document.getElementById('approve-modal-type').value = 'approve';
|
||
document.getElementById('approve-modal-document-id').value = documentId;
|
||
document.getElementById('approve-comment').value = '';
|
||
document.getElementById('refusal-reason').style.display = 'none';
|
||
document.getElementById('approve-modal').style.display = 'block';
|
||
}
|
||
|
||
function openRefuseModal(documentId) {
|
||
document.getElementById('approve-modal-type').value = 'refuse';
|
||
document.getElementById('approve-modal-document-id').value = documentId;
|
||
document.getElementById('approve-comment').value = '';
|
||
document.getElementById('refusal-reason').style.display = 'block';
|
||
document.getElementById('approve-modal').style.display = 'block';
|
||
}
|
||
|
||
function closeApproveModal() {
|
||
document.getElementById('approve-modal').style.display = 'none';
|
||
document.getElementById('approve-comment').value = '';
|
||
document.getElementById('refusal-reason-text').value = '';
|
||
}
|
||
|
||
async function submitDocumentStatus(event) {
|
||
event.preventDefault();
|
||
|
||
const documentId = document.getElementById('approve-modal-document-id').value;
|
||
const actionType = document.getElementById('approve-modal-type').value;
|
||
const comment = document.getElementById('approve-comment').value;
|
||
const refusalReason = document.getElementById('refusal-reason-text').value;
|
||
|
||
let status = '';
|
||
switch(actionType) {
|
||
case 'pre_approve': status = 'pre_approved'; break;
|
||
case 'approve': status = 'approved'; break;
|
||
case 'refuse': status = 'refused'; break;
|
||
default: return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/documents/${documentId}/status`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
status,
|
||
comment,
|
||
refusalReason: actionType === 'refuse' ? refusalReason : null
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
alert('Статус документа обновлен!');
|
||
closeApproveModal();
|
||
loadSecretaryDocuments();
|
||
} else {
|
||
const error = await response.json();
|
||
alert(error.error || 'Ошибка обновления статуса');
|
||
}
|
||
} catch (error) {
|
||
console.error('Ошибка:', error);
|
||
alert('Ошибка обновления статуса документа');
|
||
}
|
||
}
|
||
|
||
function setupDocumentFilters() {
|
||
const searchInput = document.getElementById('search-documents');
|
||
const statusFilter = document.getElementById('document-status-filter');
|
||
|
||
if (searchInput) {
|
||
searchInput.addEventListener('input', filterDocuments);
|
||
}
|
||
|
||
if (statusFilter) {
|
||
statusFilter.addEventListener('change', filterDocuments);
|
||
}
|
||
}
|
||
|
||
function filterDocuments() {
|
||
const search = document.getElementById('search-documents')?.value.toLowerCase() || '';
|
||
const statusFilter = document.getElementById('document-status-filter')?.value || 'all';
|
||
|
||
filteredDocuments = allDocuments.filter(doc => {
|
||
// Поиск по названию и номеру
|
||
const matchesSearch =
|
||
doc.title.toLowerCase().includes(search) ||
|
||
(doc.document_number && doc.document_number.toLowerCase().includes(search)) ||
|
||
(doc.description && doc.description.toLowerCase().includes(search));
|
||
|
||
if (!matchesSearch) return false;
|
||
|
||
// Фильтрация по статусу
|
||
if (statusFilter === 'all') return true;
|
||
|
||
const docStatus = getDocumentStatus(doc);
|
||
return docStatus === statusFilter;
|
||
});
|
||
|
||
// Определяем, какую секцию рендерить
|
||
const activeSection = document.querySelector('.document-section.active');
|
||
if (activeSection && activeSection.id === 'my-documents-section') {
|
||
renderMyDocuments();
|
||
} else if (activeSection && activeSection.id === 'secretary-documents-section') {
|
||
renderSecretaryDocuments();
|
||
}
|
||
}
|
||
|
||
function showDocumentSection(sectionName) {
|
||
// Скрыть все секции
|
||
document.querySelectorAll('.document-section').forEach(section => {
|
||
section.classList.remove('active');
|
||
});
|
||
|
||
// Скрыть все кнопки
|
||
document.querySelectorAll('.nav-btn').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
});
|
||
|
||
// Показать выбранную секцию
|
||
document.getElementById(sectionName + '-section').classList.add('active');
|
||
|
||
// Активировать соответствующую кнопку
|
||
const btn = document.querySelector(`.nav-btn[onclick*="${sectionName}"]`);
|
||
if (btn) btn.classList.add('active');
|
||
|
||
// Загрузить данные для секции
|
||
if (sectionName === 'my-documents') {
|
||
loadMyDocuments();
|
||
} else if (sectionName === 'secretary-documents') {
|
||
loadSecretaryDocuments();
|
||
}
|
||
} |