import React, { useState, useEffect, useRef } from 'react'; import { authFetch } from '../../services/apiClient'; import { OfficeDocument } from '../../types'; import { CURRENT_USER_MOCK } from '../../constants'; import { Search, ArrowDownLeft, ArrowUpRight, Truck, FileText, Plus, Download, CheckCircle2, X, Mail, File, Edit, Eye, Upload, Image as ImageIcon, Trash2 } from 'lucide-react'; interface Employee { id: number; name: string; position?: string; status?: string; } export const DocumentFlow: React.FC = () => { const [documents, setDocuments] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [showCreateModal, setShowCreateModal] = useState(false); const [showViewModal, setShowViewModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [selectedDocument, setSelectedDocument] = useState(null); const [isUploading, setIsUploading] = useState(false); const [employees, setEmployees] = useState([]); const fileInputRef = useRef(null); const [formData, setFormData] = useState({ regNumber: '', title: '', correspondent: '', documentType: 'incoming' as 'incoming' | 'outgoing', letterType: 'paper' as 'email' | 'paper', date: new Date().toISOString().split('T')[0], assignedTo: '', trackingNumber: '', notes: '', fileUrl: '' }); // Генерируем регистрационный номер при открытии модального окна создания useEffect(() => { if (showCreateModal && !formData.regNumber) { generateRegNumber(formData.documentType, formData.date).then(regNumber => { setFormData(prev => ({ ...prev, regNumber })); }); } }, [showCreateModal]); useEffect(() => { fetchDocuments(); fetchEmployees(); }, []); const fetchEmployees = async () => { try { const response = await authFetch('/api/employees'); if (response.ok) { const data = await response.json(); // Фильтруем только активных сотрудников const activeEmployees = data .filter((emp: Employee) => !emp.status || emp.status === 'active') .map((emp: Employee) => ({ id: emp.id, name: emp.name, position: emp.position || '' })) .sort((a: Employee, b: Employee) => a.name.localeCompare(b.name)); setEmployees(activeEmployees); } } catch (error) { console.error('Ошибка загрузки сотрудников:', error); } }; const fetchDocuments = async () => { try { const response = await authFetch('/api/office/documents'); if (response.ok) { const data = await response.json(); // Преобразуем snake_case в camelCase const normalizedData = data.map((doc: any) => ({ id: doc.id, regNumber: doc.reg_number || doc.regNumber || '', title: doc.title || '', correspondent: doc.correspondent || '', date: doc.date || '', status: doc.status || 'registered', type: doc.document_type || doc.type || 'incoming', letterType: doc.letter_type || doc.letterType || 'paper', assignedTo: doc.assigned_to || doc.assignedTo, trackingNumber: doc.tracking_number || doc.trackingNumber, fileUrl: doc.file_url || doc.fileUrl, notes: doc.notes, createdBy: doc.created_by || doc.createdBy || '', createdAt: doc.created_at, updatedAt: doc.updated_at })); setDocuments(normalizedData); } } catch (error) { console.error('Ошибка загрузки документов:', error); } finally { setLoading(false); } }; const generateRegNumber = async (documentType: string, date: string) => { try { const response = await authFetch(`/api/office/documents/next-reg-number?documentType=${documentType}&date=${date}`); if (response.ok) { const data = await response.json(); return data.regNumber; } } catch (error) { console.error('Ошибка генерации номера:', error); } // Fallback: простая генерация на клиенте const docDate = date ? new Date(date) : new Date(); const year = docDate.getFullYear().toString().slice(-2); const prefix = documentType === 'incoming' ? 'ВХ' : 'ИСХ'; const timestamp = Date.now().toString().slice(-4); return `${prefix}-${year}-${timestamp}`; }; const handleCreateDocument = async () => { try { if (!formData.title || !formData.title.trim()) { alert('Пожалуйста, укажите название документа'); return; } if (!formData.correspondent || !formData.correspondent.trim()) { alert('Пожалуйста, укажите корреспондента'); return; } // Если регистрационный номер не указан, генерируем автоматически let regNumber = formData.regNumber?.trim(); if (!regNumber) { regNumber = await generateRegNumber(formData.documentType, formData.date); } const requestBody = { reg_number: regNumber, title: formData.title.trim(), correspondent: formData.correspondent.trim(), document_type: formData.documentType, letter_type: formData.letterType, date: formData.date, assigned_to: formData.assignedTo?.trim() || null, tracking_number: formData.trackingNumber?.trim() || null, file_url: formData.fileUrl || null, notes: formData.notes?.trim() || null, created_by: CURRENT_USER_MOCK.name || 'Система' }; console.log('Создание документа:', requestBody); // Для отладки const response = await authFetch('/api/office/documents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (response.ok) { const newDoc = await response.json(); // Преобразуем snake_case в camelCase const normalizedDoc = { id: newDoc.id, regNumber: newDoc.reg_number || regNumber, title: newDoc.title || formData.title, correspondent: newDoc.correspondent || formData.correspondent, date: newDoc.date || formData.date, status: newDoc.status || 'registered', type: newDoc.document_type || formData.documentType, letterType: newDoc.letter_type || formData.letterType, assignedTo: newDoc.assigned_to || formData.assignedTo, trackingNumber: newDoc.tracking_number || formData.trackingNumber, fileUrl: newDoc.file_url || formData.fileUrl, notes: newDoc.notes || formData.notes, createdBy: newDoc.created_by || CURRENT_USER_MOCK.name, createdAt: newDoc.created_at, updatedAt: newDoc.updated_at }; setDocuments([normalizedDoc, ...documents]); setShowCreateModal(false); setFormData({ regNumber: '', title: '', correspondent: '', documentType: 'incoming', letterType: 'paper', date: new Date().toISOString().split('T')[0], assignedTo: '', trackingNumber: '', notes: '', fileUrl: '' }); } else { const errorText = await response.text(); let errorData; try { errorData = JSON.parse(errorText); } catch { errorData = { error: errorText || 'Неизвестная ошибка' }; } console.error('Ошибка создания документа:', errorData); alert(`Ошибка создания документа: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка создания документа:', error); alert(`Ошибка создания документа: ${error.message || 'Неизвестная ошибка'}`); } }; const handleViewDocument = (doc: OfficeDocument) => { setSelectedDocument(doc); setShowViewModal(true); }; const handleEditDocument = (doc: OfficeDocument) => { setSelectedDocument(doc); setFormData({ regNumber: doc.regNumber || '', title: doc.title || '', correspondent: doc.correspondent || '', documentType: doc.type || 'incoming', letterType: doc.letterType || 'paper', date: doc.date || new Date().toISOString().split('T')[0], assignedTo: doc.assignedTo || '', trackingNumber: doc.trackingNumber || '', notes: doc.notes || '', fileUrl: doc.fileUrl || '' }); setShowEditModal(true); }; const handleFileUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; setIsUploading(true); try { const formDataUpload = new FormData(); formDataUpload.append('file', file); const response = await authFetch('/api/office/documents/upload', { method: 'POST', body: formDataUpload }); if (response.ok) { const result = await response.json(); setFormData({ ...formData, fileUrl: result.url || result.fileUrl }); alert('Файл успешно загружен'); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка загрузки файла: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка загрузки файла:', error); alert(`Ошибка загрузки файла: ${error.message || 'Неизвестная ошибка'}`); } finally { setIsUploading(false); if (fileInputRef.current) { fileInputRef.current.value = ''; } } }; const handleUpdateDocument = async () => { if (!selectedDocument) return; try { if (!formData.regNumber || !formData.regNumber.trim()) { alert('Пожалуйста, укажите регистрационный номер'); return; } if (!formData.title || !formData.title.trim()) { alert('Пожалуйста, укажите название документа'); return; } if (!formData.correspondent || !formData.correspondent.trim()) { alert('Пожалуйста, укажите корреспондента'); return; } const response = await authFetch(`/api/office/documents/${selectedDocument.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reg_number: formData.regNumber, title: formData.title, correspondent: formData.correspondent, document_type: formData.documentType, letter_type: formData.letterType, date: formData.date, assigned_to: formData.assignedTo || null, tracking_number: formData.trackingNumber || null, file_url: formData.fileUrl || null, notes: formData.notes || null }) }); if (response.ok) { fetchDocuments(); setShowEditModal(false); setSelectedDocument(null); alert('Документ успешно обновлен'); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка обновления документа: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка обновления документа:', error); alert(`Ошибка обновления документа: ${error.message || 'Неизвестная ошибка'}`); } }; const handleUpdateStatus = async (docId: number | string, newStatus: string) => { try { const response = await authFetch(`/api/office/documents/${docId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: newStatus }) }); if (response.ok) { fetchDocuments(); } } catch (error) { console.error('Ошибка обновления статуса:', error); } }; const handleDeleteDocument = async (doc: OfficeDocument) => { if (!confirm(`Вы уверены, что хотите удалить документ "${doc.title}"?`)) { return; } try { const response = await authFetch(`/api/office/documents/${doc.id}`, { method: 'DELETE' }); if (response.ok) { fetchDocuments(); alert('Документ успешно удален'); } else { const errorData = await response.json().catch(() => ({ error: 'Неизвестная ошибка' })); alert(`Ошибка удаления документа: ${errorData.error || 'Неизвестная ошибка'}`); } } catch (error: any) { console.error('Ошибка удаления документа:', error); alert(`Ошибка удаления документа: ${error.message || 'Неизвестная ошибка'}`); } }; const filteredDocuments = documents.filter(doc => { const regNumber = (doc.regNumber || '').toLowerCase(); const title = (doc.title || '').toLowerCase(); const correspondent = (doc.correspondent || '').toLowerCase(); const searchLower = search.toLowerCase(); return regNumber.includes(searchLower) || title.includes(searchLower) || correspondent.includes(searchLower); }); const getStatusLabel = (status: string) => { const labels = { registered: 'Зарегистрирован', processed: 'В работе', sent: 'Отправлен', archived: 'Архивирован' }; return labels[status as keyof typeof labels] || status; }; return (
setSearch(e.target.value)} className="w-full pl-10 pr-4 py-2.5 rounded-xl border border-slate-200 text-sm focus:ring-2 focus:ring-primary-500 outline-none shadow-sm" />
{loading ? (
Загрузка...
) : filteredDocuments && filteredDocuments.length > 0 ? (
{filteredDocuments.map(doc => { if (!doc || !doc.id) return null; return (
{doc.type === 'incoming' ? : }
{doc.regNumber} {doc.date}

{doc.title}

{doc.correspondent}

{doc.letterType && (
{doc.letterType === 'email' ? ( <> Email ) : ( <> Бумага )}
)}
{getStatusLabel(doc.status)} {doc.trackingNumber && (
{doc.trackingNumber}
)}
{doc.fileUrl && ( )}
); }).filter(Boolean)}
) : (
Нет документов
)}

Цифровой архив

Всего {documents.length} документов в системе

{/* Create Document Modal */} {showCreateModal && (

Регистрация документа

setFormData({ ...formData, regNumber: e.target.value })} className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm" placeholder="Автоматически сгенерируется" />
setFormData({ ...formData, title: e.target.value })} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" placeholder="Введите название документа" required />
setFormData({ ...formData, correspondent: e.target.value })} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" placeholder="Введите название организации" required />
setFormData({ ...formData, date: e.target.value })} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" required />
{employees.length === 0 && (

Загрузка списка сотрудников...

)}
{formData.documentType === 'outgoing' && (
setFormData({ ...formData, trackingNumber: e.target.value })} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm" placeholder="Номер для отслеживания (необязательно)" />
)}
{formData.fileUrl && ( Файл загружен )}

Скан или текстовый файл (PDF, DOC, TXT, изображения)