import React, { useState, useEffect } from 'react'; import { LegalContract, ContractStatus } from '../../types'; import { Search, FileText, FileSignature, CheckCircle2, Clock, ChevronRight, Archive, Inbox, Plus, X, Loader2, Paperclip, History, ExternalLink } from 'lucide-react'; import { authFetch } from '../../services/apiClient'; const STATUS_OPTIONS: { value: ContractStatus; label: string }[] = [ { value: 'draft', label: 'Черновик' }, { value: 'finance_approval', label: 'Согл. Фин.' }, { value: 'counterparty_approval', label: 'Согл. Контр.' }, { value: 'signing', label: 'На подписи' }, { value: 'active', label: 'Подписан' }, { value: 'archived', label: 'Архив' }, ]; const STATUS_MAP: Record = { draft: { label: 'Черновик', color: 'text-slate-600', bg: 'bg-slate-100' }, finance_approval: { label: 'Согл. Фин.', color: 'text-amber-600', bg: 'bg-amber-100' }, counterparty_approval: { label: 'Согл. Контр.', color: 'text-blue-600', bg: 'bg-blue-100' }, signing: { label: 'На подписи', color: 'text-purple-600', bg: 'bg-purple-100' }, active: { label: 'Подписан', color: 'text-emerald-600', bg: 'bg-emerald-100' }, archived: { label: 'Архив', color: 'text-slate-400', bg: 'bg-slate-50' }, }; export const ContractsRegistry: React.FC = () => { const [viewMode, setViewMode] = useState<'active' | 'archive'>('active'); const [statusFilter, setStatusFilter] = useState('all'); const [search, setSearch] = useState(''); const [contracts, setContracts] = useState([]); const [loading, setLoading] = useState(true); const [showModal, setShowModal] = useState(false); const [editingContract, setEditingContract] = useState(null); useEffect(() => { loadContracts(); }, [viewMode, statusFilter]); const loadContracts = async () => { try { setLoading(true); let url = `/api/legal/contracts?viewMode=${viewMode}&search=${encodeURIComponent(search)}`; if (statusFilter && statusFilter !== 'all') url += `&status=${encodeURIComponent(statusFilter)}`; const response = await authFetch(url); if (response.ok) { const data = await response.json(); setContracts(data); } } catch (error) { console.error('Error loading contracts:', error); } finally { setLoading(false); } }; useEffect(() => { const timeoutId = setTimeout(() => { if (search !== undefined) { loadContracts(); } }, 300); return () => clearTimeout(timeoutId); }, [search]); const uploadsBase = (import.meta.env.VITE_API_BASE_URL || '').replace(/\/api\/?$/, '') || ''; const handleCreate = () => { setEditingContract(null); setShowModal(true); }; const handleEdit = (contract: LegalContract) => { setEditingContract(contract); setShowModal(true); }; const handleSave = async (contractData: Partial) => { try { if (editingContract) { // Обновление const response = await authFetch(`/api/legal/contracts/${editingContract.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(contractData) }); if (response.ok) { await loadContracts(); setShowModal(false); setEditingContract(null); } } else { // Создание const response = await authFetch('/api/legal/contracts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(contractData) }); if (response.ok) { await loadContracts(); setShowModal(false); } } } catch (error) { console.error('Error saving contract:', error); alert('Ошибка при сохранении договора'); } }; const filtered = contracts.filter(c => { const searchMatch = !search || c.counterparty.toLowerCase().includes(search.toLowerCase()) || c.number.toLowerCase().includes(search.toLowerCase()); return searchMatch; }); return (
{/* Top Toggle & Search & Create Button */}
setSearch(e.target.value)} className="w-full pl-11 pr-4 py-3 bg-white border border-slate-200 rounded-2xl text-sm outline-none focus:ring-2 focus:ring-primary-500 transition-all shadow-sm" />
{viewMode === 'active' && ( )}
{/* List */} {loading ? (
) : (
{filtered.map(contract => { const status = STATUS_MAP[contract.status]; return (
handleEdit(contract)} >
{status.label} № {contract.number}

{contract.counterparty}

{contract.amount.toLocaleString()} ₽

{contract.type}

до {contract.endDate}
{contract.autoProlongation && (
Автопролонгация
)} {contract.contractFileUrl && ( e.stopPropagation()} className="inline-flex items-center gap-1 text-[10px] font-black text-primary-600 uppercase hover:text-primary-700" > Документ )}
Открыть
); })} {filtered.length === 0 && (

Договоры не найдены

{viewMode === 'active' && ( )}
)}
)} {/* Contract Modal */} {showModal && ( { setShowModal(false); setEditingContract(null); }} onSave={handleSave} /> )}
); }; interface ContractModalProps { contract: LegalContract | null; onClose: () => void; onSave: (data: Partial) => Promise; } interface ContractHistoryEntry { fromStatus: string | null; toStatus: string; changedBy: string; createdAt: string; } const ContractModal: React.FC = ({ contract, onClose, onSave }) => { const [loading, setLoading] = useState(false); const [uploading, setUploading] = useState(false); const [history, setHistory] = useState([]); const [formData, setFormData] = useState({ number: contract?.number || '', type: contract?.type || '', counterparty: contract?.counterparty || '', counterpartyInn: contract?.counterpartyInn ?? '', amount: contract?.amount || 0, startDate: contract?.startDate || '', endDate: contract?.endDate || '', autoProlongation: contract?.autoProlongation ?? false, manager: contract?.manager || '', notes: contract?.notes ?? '', status: (contract?.status || 'draft') as ContractStatus, hasDisagreements: contract?.hasDisagreements ?? false, contractFileUrl: contract?.contractFileUrl ?? '', }); useEffect(() => { setFormData({ number: contract?.number || '', type: contract?.type || '', counterparty: contract?.counterparty || '', counterpartyInn: contract?.counterpartyInn ?? '', amount: contract?.amount || 0, startDate: contract?.startDate || '', endDate: contract?.endDate || '', autoProlongation: contract?.autoProlongation ?? false, manager: contract?.manager || '', notes: contract?.notes ?? '', status: (contract?.status || 'draft') as ContractStatus, hasDisagreements: contract?.hasDisagreements ?? false, contractFileUrl: contract?.contractFileUrl ?? '', }); if (contract?.id) { authFetch(`/api/legal/contracts/${contract.id}/history`) .then((r) => r.ok ? r.json() : []) .then(setHistory) .catch(() => setHistory([])); } else { setHistory([]); } }, [contract?.id]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); try { await onSave(formData); } finally { setLoading(false); } }; const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setUploading(true); try { const fd = new FormData(); fd.append('file', file); const res = await authFetch('/api/legal/contracts/upload', { method: 'POST', body: fd, }); if (res.ok) { const data = await res.json(); setFormData((prev) => ({ ...prev, contractFileUrl: data.fileUrl || '' })); } } catch (err) { console.error('Upload error:', err); alert('Ошибка загрузки файла'); } finally { setUploading(false); e.target.value = ''; } }; const uploadsBase = (import.meta.env.VITE_API_BASE_URL || '').replace(/\/api\/?$/, '') || ''; return (

{contract ? 'Редактировать договор' : 'Создать договор'}

setFormData({ ...formData, number: e.target.value })} className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500" required />
setFormData({ ...formData, counterparty: e.target.value })} className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500" required />
setFormData({ ...formData, counterpartyInn: e.target.value.replace(/\D/g, '') })} className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500" maxLength={12} placeholder="10 или 12 цифр" />
setFormData({ ...formData, amount: parseFloat(e.target.value) || 0 })} className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500" min="0" step="0.01" required />
setFormData({ ...formData, manager: e.target.value })} className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500" required />
{contract && (
)}
setFormData({ ...formData, startDate: e.target.value })} className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500" required />
setFormData({ ...formData, endDate: e.target.value })} className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500" required />
setFormData({ ...formData, autoProlongation: e.target.checked })} className="w-4 h-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500" />
setFormData({ ...formData, hasDisagreements: e.target.checked })} className="w-4 h-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500" />
{formData.contractFileUrl ? (
Открыть документ
) : ( )}