Files
mkd/components/office/DocumentFlow.tsx
2026-02-04 00:17:04 +05:00

1023 lines
57 KiB
TypeScript
Executable File
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.
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<OfficeDocument[]>([]);
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<OfficeDocument | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [employees, setEmployees] = useState<Employee[]>([]);
const fileInputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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 (
<div className="space-y-4 animate-fade-in">
<div className="flex gap-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Поиск по номеру, отправителю..."
value={search}
onChange={(e) => 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"
/>
</div>
<button
onClick={async () => {
const newRegNumber = await generateRegNumber('incoming', new Date().toISOString().split('T')[0]);
setFormData({
regNumber: newRegNumber,
title: '',
correspondent: '',
documentType: 'incoming',
letterType: 'paper',
date: new Date().toISOString().split('T')[0],
assignedTo: '',
trackingNumber: '',
notes: '',
fileUrl: ''
});
setShowCreateModal(true);
}}
className="bg-primary-600 text-white p-2.5 rounded-xl shadow-lg active:scale-95 transition-all flex items-center gap-2 px-4 text-sm font-bold"
>
<Plus className="w-5 h-5"/>
Создать
</button>
</div>
{loading ? (
<div className="text-center py-8 text-slate-500">Загрузка...</div>
) : filteredDocuments && filteredDocuments.length > 0 ? (
<div className="space-y-3">
{filteredDocuments.map(doc => {
if (!doc || !doc.id) return null;
return (
<div key={doc.id} className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm flex items-center gap-4 group">
<div className={`p-3 rounded-2xl flex-shrink-0 border ${
doc.type === 'incoming' ? 'bg-emerald-50 border-emerald-100 text-emerald-600' : 'bg-blue-50 border-blue-100 text-blue-600'
}`}>
{doc.type === 'incoming' ? <ArrowDownLeft className="w-5 h-5"/> : <ArrowUpRight className="w-5 h-5"/>}
</div>
<div className="flex-1 min-w-0">
<div className="flex justify-between items-start mb-0.5">
<span className="text-[10px] font-black bg-slate-100 text-slate-500 px-2 py-0.5 rounded tracking-wider">{doc.regNumber}</span>
<span className="text-[10px] text-slate-400 font-bold">{doc.date}</span>
</div>
<h4 className="font-bold text-slate-800 text-sm truncate group-hover:text-primary-600 transition-colors">{doc.title}</h4>
<div className="flex items-center gap-2 mt-1">
<p className="text-[11px] text-slate-500 truncate font-medium flex-1">{doc.correspondent}</p>
{doc.letterType && (
<div className="flex items-center gap-1 text-[9px] font-bold text-slate-500 bg-slate-50 px-2 py-0.5 rounded border border-slate-200">
{doc.letterType === 'email' ? (
<>
<Mail className="w-3 h-3" />
Email
</>
) : (
<>
<FileText className="w-3 h-3" />
Бумага
</>
)}
</div>
)}
</div>
<div className="mt-3 flex items-center justify-between">
<span className="text-[9px] font-black text-primary-600 bg-primary-50 px-2 py-0.5 rounded uppercase tracking-tighter border border-primary-100">
{getStatusLabel(doc.status)}
</span>
{doc.trackingNumber && (
<div className="flex items-center gap-1.5 text-[9px] font-bold text-slate-500 bg-slate-50 px-2 py-0.5 rounded border border-slate-200">
<Truck className="w-3 h-3"/> {doc.trackingNumber}
</div>
)}
</div>
</div>
<div className="flex gap-2 flex-shrink-0">
<button
onClick={() => handleViewDocument(doc)}
className="p-2 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition-colors opacity-0 group-hover:opacity-100"
title="Просмотреть"
>
<Eye className="w-5 h-5"/>
</button>
<button
onClick={() => handleEditDocument(doc)}
className="p-2 bg-amber-50 text-amber-600 rounded-lg hover:bg-amber-100 transition-colors opacity-0 group-hover:opacity-100"
title="Редактировать"
>
<Edit className="w-5 h-5"/>
</button>
<button
onClick={() => handleDeleteDocument(doc)}
className="p-2 bg-red-50 text-red-600 rounded-lg hover:bg-red-100 transition-colors opacity-0 group-hover:opacity-100"
title="Удалить"
>
<Trash2 className="w-5 h-5"/>
</button>
{doc.fileUrl && (
<a
href={doc.fileUrl}
target="_blank"
rel="noopener noreferrer"
className="p-2 text-slate-300 hover:text-primary-600 transition-colors opacity-0 group-hover:opacity-100"
title="Скачать файл"
>
<Download className="w-5 h-5"/>
</a>
)}
</div>
</div>
);
}).filter(Boolean)}
</div>
) : (
<div className="text-center py-8 text-slate-500">Нет документов</div>
)}
<div className="bg-slate-100 rounded-2xl p-6 border-2 border-dashed border-slate-200 flex flex-col items-center justify-center text-slate-400">
<FileText className="w-10 h-10 mb-2 opacity-20"/>
<p className="text-xs font-black uppercase tracking-widest">Цифровой архив</p>
<p className="text-[10px] mt-1">Всего {documents.length} документов в системе</p>
</div>
{/* Create Document Modal */}
{showCreateModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold text-slate-800">Регистрация документа</h3>
<button
onClick={() => setShowCreateModal(false)}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Тип документа</label>
<select
value={formData.documentType}
onChange={async (e) => {
const newType = e.target.value as any;
const newRegNumber = await generateRegNumber(newType, formData.date);
setFormData({ ...formData, documentType: newType, regNumber: newRegNumber });
}}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
<option value="incoming">Входящий</option>
<option value="outgoing">Исходящий</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Тип письма</label>
<select
value={formData.letterType}
onChange={(e) => setFormData({ ...formData, letterType: e.target.value as any })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
<option value="email">Email</option>
<option value="paper">Бумага</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Регистрационный номер
<span className="text-xs text-slate-400 ml-2">(автоматически, можно изменить)</span>
</label>
<div className="flex gap-2">
<input
type="text"
value={formData.regNumber}
onChange={(e) => setFormData({ ...formData, regNumber: e.target.value })}
className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm"
placeholder="Автоматически сгенерируется"
/>
<button
type="button"
onClick={async () => {
const newRegNumber = await generateRegNumber(formData.documentType, formData.date);
setFormData({ ...formData, regNumber: newRegNumber });
}}
className="px-3 py-2 bg-slate-100 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-200 transition-colors"
title="Сгенерировать новый номер"
>
Обновить
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Название *</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
placeholder="Введите название документа"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Корреспондент *</label>
<input
type="text"
value={formData.correspondent}
onChange={(e) => setFormData({ ...formData, correspondent: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
placeholder="Введите название организации"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Дата *</label>
<input
type="date"
value={formData.date}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Назначить сотруднику</label>
<select
value={formData.assignedTo}
onChange={(e) => setFormData({ ...formData, assignedTo: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
<option value="">Не назначено</option>
{employees.map((emp) => (
<option key={emp.id} value={emp.name}>
{emp.name}{emp.position ? ` - ${emp.position}` : ''}
</option>
))}
</select>
{employees.length === 0 && (
<p className="text-xs text-slate-400 mt-1">Загрузка списка сотрудников...</p>
)}
</div>
{formData.documentType === 'outgoing' && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Трек-номер</label>
<input
type="text"
value={formData.trackingNumber}
onChange={(e) => setFormData({ ...formData, trackingNumber: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
placeholder="Номер для отслеживания (необязательно)"
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Загрузить оригинал</label>
<div className="flex items-center gap-3">
<input
ref={fileInputRef}
type="file"
onChange={handleFileUpload}
className="hidden"
accept="image/*,.pdf,.doc,.docx,.txt"
/>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-200 transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Upload className="w-4 h-4" />
{isUploading ? 'Загрузка...' : 'Выбрать файл'}
</button>
{formData.fileUrl && (
<span className="text-xs text-slate-500 flex items-center gap-1">
<File className="w-3 h-3" />
Файл загружен
</span>
)}
</div>
<p className="text-xs text-slate-400 mt-1">Скан или текстовый файл (PDF, DOC, TXT, изображения)</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Примечания</label>
<textarea
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
rows={3}
placeholder="Дополнительная информация"
/>
</div>
<div className="flex gap-3 pt-4">
<button
onClick={handleCreateDocument}
disabled={!formData.title || !formData.correspondent}
className="flex-1 bg-primary-600 text-white px-4 py-2 rounded-lg font-bold text-sm hover:bg-primary-700 transition-colors disabled:bg-slate-300 disabled:cursor-not-allowed"
>
Зарегистрировать
</button>
<button
onClick={() => setShowCreateModal(false)}
className="px-4 py-2 bg-slate-100 text-slate-600 rounded-lg font-bold text-sm hover:bg-slate-200 transition-colors"
>
Отмена
</button>
</div>
</div>
</div>
</div>
)}
{/* View Document Modal */}
{showViewModal && selectedDocument && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 max-w-3xl w-full max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-6">
<div className="flex items-center gap-3">
<div className={`p-3 rounded-2xl ${
selectedDocument.type === 'incoming' ? 'bg-emerald-50 text-emerald-600' : 'bg-blue-50 text-blue-600'
}`}>
{selectedDocument.type === 'incoming' ? <ArrowDownLeft className="w-6 h-6"/> : <ArrowUpRight className="w-6 h-6"/>}
</div>
<div>
<h3 className="text-xl font-bold text-slate-800">{selectedDocument.title}</h3>
<p className="text-sm text-slate-500 mt-1">{selectedDocument.correspondent}</p>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => {
setShowViewModal(false);
handleEditDocument(selectedDocument);
}}
className="p-2 bg-amber-50 text-amber-600 rounded-lg hover:bg-amber-100 transition-colors"
title="Редактировать"
>
<Edit className="w-5 h-5" />
</button>
<button
onClick={() => {
setShowViewModal(false);
if (selectedDocument && confirm(`Вы уверены, что хотите удалить документ "${selectedDocument.title}"?`)) {
handleDeleteDocument(selectedDocument);
}
}}
className="p-2 bg-red-50 text-red-600 rounded-lg hover:bg-red-100 transition-colors"
title="Удалить"
>
<Trash2 className="w-5 h-5" />
</button>
<button
onClick={() => {
setShowViewModal(false);
setSelectedDocument(null);
}}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="bg-slate-50 p-4 rounded-xl">
<p className="text-xs font-bold text-slate-400 uppercase mb-1">Регистрационный номер</p>
<p className="text-sm font-bold text-slate-800">{selectedDocument.regNumber}</p>
</div>
<div className="bg-slate-50 p-4 rounded-xl">
<p className="text-xs font-bold text-slate-400 uppercase mb-1">Дата</p>
<p className="text-sm font-bold text-slate-800">{selectedDocument.date}</p>
</div>
<div className="bg-slate-50 p-4 rounded-xl">
<p className="text-xs font-bold text-slate-400 uppercase mb-1">Тип письма</p>
<div className="flex items-center gap-2">
{selectedDocument.letterType === 'email' ? (
<Mail className="w-4 h-4 text-blue-600" />
) : (
<FileText className="w-4 h-4 text-amber-600" />
)}
<p className="text-sm font-bold text-slate-800">
{selectedDocument.letterType === 'email' ? 'Email' : 'Бумага'}
</p>
</div>
</div>
<div className="bg-slate-50 p-4 rounded-xl">
<p className="text-xs font-bold text-slate-400 uppercase mb-1">Статус</p>
<span className="text-xs font-black text-primary-600 bg-primary-50 px-2 py-1 rounded uppercase tracking-tighter border border-primary-100">
{getStatusLabel(selectedDocument.status)}
</span>
</div>
</div>
{selectedDocument.assignedTo && (
<div className="bg-slate-50 p-4 rounded-xl">
<p className="text-xs font-bold text-slate-400 uppercase mb-1">Назначен</p>
<p className="text-sm font-bold text-slate-800">{selectedDocument.assignedTo}</p>
</div>
)}
{selectedDocument.trackingNumber && (
<div className="bg-slate-50 p-4 rounded-xl">
<p className="text-xs font-bold text-slate-400 uppercase mb-1">Трек-номер</p>
<div className="flex items-center gap-2">
<Truck className="w-4 h-4 text-slate-500" />
<p className="text-sm font-bold text-slate-800">{selectedDocument.trackingNumber}</p>
</div>
</div>
)}
{selectedDocument.fileUrl && (
<div className="bg-slate-50 p-4 rounded-xl">
<p className="text-xs font-bold text-slate-400 uppercase mb-2">Оригинал</p>
<a
href={selectedDocument.fileUrl}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm font-bold text-primary-600 hover:text-primary-700 transition-colors"
>
<Download className="w-4 h-4" />
Скачать файл
</a>
</div>
)}
{selectedDocument.notes && (
<div className="bg-slate-50 p-4 rounded-xl">
<p className="text-xs font-bold text-slate-400 uppercase mb-1">Примечания</p>
<p className="text-sm text-slate-700 whitespace-pre-wrap">{selectedDocument.notes}</p>
</div>
)}
<div className="pt-4 border-t border-slate-200 flex items-center justify-between text-xs text-slate-500">
<span>Создано: {selectedDocument.createdBy}</span>
{selectedDocument.createdAt && (
<span>{new Date(selectedDocument.createdAt).toLocaleString('ru-RU')}</span>
)}
</div>
</div>
</div>
</div>
)}
{/* Edit Document Modal */}
{showEditModal && selectedDocument && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold text-slate-800">Редактировать документ</h3>
<button
onClick={() => {
setShowEditModal(false);
setSelectedDocument(null);
}}
className="p-2 hover:bg-slate-100 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Тип документа</label>
<select
value={formData.documentType}
onChange={(e) => setFormData({ ...formData, documentType: e.target.value as any })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
<option value="incoming">Входящий</option>
<option value="outgoing">Исходящий</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Тип письма</label>
<select
value={formData.letterType}
onChange={(e) => setFormData({ ...formData, letterType: e.target.value as any })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
<option value="email">Email</option>
<option value="paper">Бумага</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Регистрационный номер *</label>
<input
type="text"
value={formData.regNumber}
onChange={(e) => setFormData({ ...formData, regNumber: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Название *</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Корреспондент *</label>
<input
type="text"
value={formData.correspondent}
onChange={(e) => setFormData({ ...formData, correspondent: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Дата *</label>
<input
type="date"
value={formData.date}
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Назначить сотруднику</label>
<select
value={formData.assignedTo}
onChange={(e) => setFormData({ ...formData, assignedTo: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
<option value="">Не назначено</option>
{employees.map((emp) => (
<option key={emp.id} value={emp.name}>
{emp.name}{emp.position ? ` - ${emp.position}` : ''}
</option>
))}
</select>
{employees.length === 0 && (
<p className="text-xs text-slate-400 mt-1">Загрузка списка сотрудников...</p>
)}
</div>
{formData.documentType === 'outgoing' && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Трек-номер</label>
<input
type="text"
value={formData.trackingNumber}
onChange={(e) => setFormData({ ...formData, trackingNumber: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
placeholder="Номер для отслеживания (необязательно)"
/>
</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Загрузить оригинал</label>
<div className="flex items-center gap-3">
<input
ref={fileInputRef}
type="file"
onChange={handleFileUpload}
className="hidden"
accept="image/*,.pdf,.doc,.docx,.txt"
/>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-200 transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Upload className="w-4 h-4" />
{isUploading ? 'Загрузка...' : formData.fileUrl ? 'Заменить файл' : 'Выбрать файл'}
</button>
{formData.fileUrl && (
<span className="text-xs text-slate-500 flex items-center gap-1">
<File className="w-3 h-3" />
Файл загружен
</span>
)}
</div>
<p className="text-xs text-slate-400 mt-1">Скан или текстовый файл (PDF, DOC, TXT, изображения)</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Примечания</label>
<textarea
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
rows={3}
placeholder="Дополнительная информация"
/>
</div>
<div className="flex gap-3 pt-4">
<button
onClick={handleUpdateDocument}
disabled={!formData.regNumber || !formData.title || !formData.correspondent}
className="flex-1 bg-primary-600 text-white px-4 py-2 rounded-lg font-bold text-sm hover:bg-primary-700 transition-colors disabled:bg-slate-300 disabled:cursor-not-allowed"
>
Сохранить
</button>
<button
onClick={() => {
setShowEditModal(false);
setSelectedDocument(null);
}}
className="px-4 py-2 bg-slate-100 text-slate-600 rounded-lg font-bold text-sm hover:bg-slate-200 transition-colors"
>
Отмена
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};