import React, { useState, useEffect, useMemo } from 'react'; import { LegalCourtCase } from '../../types'; import { Gavel, ExternalLink, Calendar, CreditCard, Landmark, ChevronRight, Plus, X, Loader2, Search } from 'lucide-react'; import { authFetch } from '../../services/apiClient'; import { CaseDetailsModal } from './CaseDetailsModal'; /** Ссылка на Картотеку арбитражных дел (КАД) или сайт судов общей юрисдикции (СОЮ) */ function getCourtCaseExternalUrl(type: string, caseNumber: string): { url: string; label: string } { const encoded = encodeURIComponent(caseNumber.trim()); if (type === 'arbitration') { return { url: `https://kad.arbitr.ru/?q=${encoded}`, label: 'КАД' }; } return { url: 'https://sudrf.ru/', label: 'СОЮ' }; } type SubTab = 'all' | 'debtors' | 'others'; export const CourtCases: React.FC = () => { const [cases, setCases] = useState([]); const [loading, setLoading] = useState(true); const [showModal, setShowModal] = useState(false); const [editingCase, setEditingCase] = useState(null); const [selectedCase, setSelectedCase] = useState(null); const [showCaseDetailsModal, setShowCaseDetailsModal] = useState(false); const [subTab, setSubTab] = useState('all'); const [statusFilter, setStatusFilter] = useState(''); const [roleFilter, setRoleFilter] = useState(''); const [searchQuery, setSearchQuery] = useState(''); const loadCases = async () => { try { setLoading(true); const params = new URLSearchParams(); if (searchQuery.trim()) params.set('search', searchQuery.trim()); const response = await authFetch(`/api/legal/court-cases${params.toString() ? '?' + params.toString() : ''}`); if (response.ok) { const data = await response.json(); setCases(Array.isArray(data) ? data : []); } else { setCases([]); } } catch (error) { console.error('Error loading court cases:', error); setCases([]); } finally { setLoading(false); } }; useEffect(() => { if (searchQuery === '') { loadCases(); return; } const t = setTimeout(() => loadCases(), 300); return () => clearTimeout(t); }, [searchQuery]); const filteredCases = useMemo(() => { let list = cases; if (subTab === 'debtors') list = list.filter(c => c.type === 'debt_recovery'); else if (subTab === 'others') list = list.filter(c => c.type === 'arbitration' || c.type === 'civil'); if (statusFilter) list = list.filter(c => c.status === statusFilter); if (roleFilter) list = list.filter(c => c.role === roleFilter); return list; }, [cases, subTab, statusFilter, roleFilter]); const countsByType = useMemo(() => ({ arbitration: filteredCases.filter(c => c.type === 'arbitration').length, civil: filteredCases.filter(c => c.type === 'civil').length, debt_recovery: filteredCases.filter(c => c.type === 'debt_recovery').length }), [filteredCases]); const upcomingHearings = useMemo(() => { const today = new Date(); const from = new Date(today); const to = new Date(today); to.setDate(to.getDate() + 30); return filteredCases .filter(c => c.nextHearingDate && c.status !== 'closed') .map(c => ({ ...c, hearingDate: new Date(c.nextHearingDate!) })) .filter(c => c.hearingDate >= from && c.hearingDate <= to) .sort((a, b) => a.hearingDate.getTime() - b.hearingDate.getTime()) .slice(0, 5); }, [filteredCases]); const handleCreate = () => { setEditingCase(null); setShowModal(true); }; const handleEdit = (courtCase: LegalCourtCase) => { setEditingCase(courtCase); setShowModal(true); }; const handleOpenCard = (courtCase: LegalCourtCase) => { setSelectedCase(courtCase); setShowCaseDetailsModal(true); }; const handleCaseDetailsUpdate = (updatedCase: LegalCourtCase) => { setSelectedCase(updatedCase); setCases(prev => prev.map(c => c.id === updatedCase.id ? { ...c, ...updatedCase } : c)); }; const handleSave = async (caseData: Partial) => { try { if (editingCase) { // Обновление const response = await authFetch(`/api/legal/court-cases/${editingCase.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(caseData) }); if (response.ok) { await loadCases(); setShowModal(false); setEditingCase(null); } } else { // Создание const response = await authFetch('/api/legal/court-cases', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(caseData) }); if (response.ok) { await loadCases(); setShowModal(false); } } } catch (error) { console.error('Error saving court case:', error); alert('Ошибка при сохранении судебного дела'); } }; const handleExport = () => { const headers = ['Номер дела', 'Тип', 'Роль', 'Предмет', 'Должник', 'Адрес', 'Сумма', 'Статус', 'Дата заседания', 'Судья', 'Суд', 'Взыскано', 'У приставов']; const statusLabel = (s: string) => ({ pre_trial: 'Досудебное', litigation: 'В суде', decision_received: 'Решение получено', enforcement: 'ФССП', closed: 'Закрыто' })[s] || s; const typeLabel = (t: string) => ({ arbitration: 'Арбитраж', civil: 'СОЮ', debt_recovery: 'Взыскание долга' })[t] || t; const roleLabel = (r: string) => ({ plaintiff: 'Истец', defendant: 'Ответчик' })[r] || r; const escape = (v: string | number | undefined) => { const s = String(v ?? ''); return s.includes(';') || s.includes('"') || s.includes('\n') ? `"${s.replace(/"/g, '""')}"` : s; }; const rows = filteredCases.map(c => [ escape(c.caseNumber), escape(typeLabel(c.type)), escape(roleLabel(c.role)), escape(c.subject), escape(c.debtorName), escape(c.address), escape(c.amount), escape(statusLabel(c.status)), escape(c.nextHearingDate), escape(c.judge), escape(c.courtName ?? ''), escape(c.recoveredAmount ?? ''), escape(c.amountAtBailiffs ?? '') ].join(';')); const csv = '\uFEFF' + [headers.join(';'), ...rows].join('\r\n'); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `court-cases-${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); }; return (

Судебная практика УК

{/* Подтабы */}
{[ { id: 'all' as SubTab, label: 'Все' }, { id: 'debtors' as SubTab, label: 'По должникам' }, { id: 'others' as SubTab, label: 'Другие' } ].map(tab => ( ))}
{subTab === 'debtors' && (

Подробный пайплайн и контроль приставов — во вкладке Взыскание.

)} {/* Фильтры и поиск */}
setSearchQuery(e.target.value)} className="w-full pl-9 pr-4 py-2.5 bg-white border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-primary-500" />
{loading ? (
) : ( <> {/* Сводка: счётчики по типам и ближайшие заседания */}

Дела по типам

Арбитраж: {countsByType.arbitration} СОЮ: {countsByType.civil} Взыскание долга: {countsByType.debt_recovery}

Ближайшие заседания (30 дней)

{upcomingHearings.length === 0 ? (

Нет назначенных заседаний

) : (
    {upcomingHearings.map(c => (
  • handleOpenCard(c)} className="flex justify-between items-center text-sm cursor-pointer hover:bg-slate-50 rounded-lg px-2 py-1 -mx-2 -my-1" > {c.caseNumber} {c.nextHearingDate}
  • ))}
)}
{filteredCases.map(c => (
handleOpenCard(c)} >
{c.caseNumber} {(() => { const { url, label } = getCourtCaseExternalUrl(c.type, c.caseNumber); return ( e.stopPropagation()} title={`Открыть в ${label}`}> {label} ); })()}

{c.type === 'arbitration' ? 'Арбитражный суд' : c.type === 'civil' ? 'Суд общей юрисдикции' : 'Взыскание долга'}

{c.role === 'plaintiff' ? 'Истец' : 'Ответчик'}

{c.subject}

Сумма иска

{c.amount.toLocaleString()} ₽

Заседание

{c.nextHearingDate || 'Не назначено'}

Статус: {c.status === 'pre_trial' ? 'Досудебное' : c.status === 'litigation' ? 'Судебный процесс' : c.status === 'decision_received' ? 'Решение получено' : c.status === 'enforcement' ? 'ФССП' : 'Завершено'}
))} {filteredCases.length === 0 && (

Судебные дела не найдены

)}
)} {/* Court Case Modal */} {showModal && ( { setShowModal(false); setEditingCase(null); }} onSave={handleSave} /> )} {showCaseDetailsModal && selectedCase && ( { setShowCaseDetailsModal(false); setSelectedCase(null); }} onUpdate={handleCaseDetailsUpdate} /> )}
); }; interface CourtCaseModalProps { courtCase: LegalCourtCase | null; onClose: () => void; onSave: (data: Partial) => Promise; } const CourtCaseModal: React.FC = ({ courtCase, onClose, onSave }) => { const [loading, setLoading] = useState(false); const [formData, setFormData] = useState({ caseNumber: courtCase?.caseNumber || '', type: courtCase?.type || 'debt_recovery', role: courtCase?.role || 'plaintiff', subject: courtCase?.subject || '', debtorName: courtCase?.debtorName || '', address: courtCase?.address || '', amount: courtCase?.amount || 0, nextHearingDate: courtCase?.nextHearingDate || '', judge: courtCase?.judge || '', courtName: courtCase?.courtName ?? '', notes: courtCase?.notes ?? '' }); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); try { await onSave(formData); } finally { setLoading(false); } }; return (

{courtCase ? 'Редактировать судебное дело' : 'Создать судебное дело'}

setFormData({ ...formData, caseNumber: 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" placeholder="А40-12345/2024" required />