Files
minicrm/public/doc.js
2026-02-02 10:24:37 +05:00

568 lines
24 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.
// 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();
}
}