import React, { useState, useEffect } from 'react'; import { LegalCourtCase, FsspStage } from '../../types'; import { HandCoins, Gavel, ShieldAlert, CheckCircle2, ArrowRight, Search, Filter, TrendingUp, Landmark, MessageSquare, AlertCircle, ChevronRight, Zap, CircleDollarSign, Receipt, Clock, AlertTriangle, Plus, Loader2, X, FileText, History, User, Calendar, DollarSign, Edit, Save } from 'lucide-react'; import { authFetch } from '../../services/apiClient'; import { CaseDetailsModal } from './CaseDetailsModal'; interface DebtRecoveryPipelineProps { onNavigateToPreTrial?: () => void; } interface PreTrialWorkItem { id: number; status: string; debtor?: { debtorName?: string; address?: string; debt_amount?: number; apartment?: string }; debtorId?: number; } export const DebtRecoveryPipeline: React.FC = ({ onNavigateToPreTrial }) => { const [search, setSearch] = useState(''); const [viewMode, setViewMode] = useState<'pipeline' | 'fssp_active' | 'pipeline_board'>('pipeline'); const [debtCases, setDebtCases] = useState([]); const [preTrialWorks, setPreTrialWorks] = useState([]); const [loading, setLoading] = useState(true); const [selectedCase, setSelectedCase] = useState(null); const [showCaseModal, setShowCaseModal] = useState(false); const [showBailiffModal, setShowBailiffModal] = useState(false); const loadCases = async () => { try { setLoading(true); const query = new URLSearchParams({ type: 'debt_recovery' }); if (search.trim()) query.set('search', search.trim()); const response = await authFetch(`/api/legal/court-cases?${query.toString()}`); if (response.ok) { const data = await response.json(); // Фильтруем дела типа debt_recovery или содержащие "взыскание" в предмете const filtered = Array.isArray(data) ? data.filter((c: LegalCourtCase) => c.type === 'debt_recovery' || (c.subject && c.subject.toLowerCase().includes('взыскание')) || (c.subject && c.subject.toLowerCase().includes('задолженност')) ) : []; setDebtCases(filtered); } else { setDebtCases([]); } } catch (error) { console.error('Error loading court cases:', error); setDebtCases([]); } finally { setLoading(false); } }; const loadPreTrialWorks = async () => { try { const response = await authFetch('/api/legal/pre-trial-work'); if (response.ok) { const data = await response.json(); const list = Array.isArray(data) ? data : []; const notTransferred = list.filter((w: PreTrialWorkItem) => w.status !== 'transferred_to_court' && w.status !== 'resolved'); setPreTrialWorks(notTransferred); } else { setPreTrialWorks([]); } } catch (e) { console.error('Error loading pre-trial work:', e); setPreTrialWorks([]); } }; useEffect(() => { if (search === '') { loadCases(); return; } const t = setTimeout(() => loadCases(), 300); return () => clearTimeout(t); }, [search]); useEffect(() => { if (viewMode === 'pipeline_board') { loadPreTrialWorks(); } }, [viewMode]); const totalClaimed = debtCases.length > 0 ? debtCases.reduce((sum, c) => sum + (c.amount || 0), 0) : 0; const totalRecovered = debtCases.length > 0 ? debtCases.reduce((sum, c) => sum + (c.recoveredAmount || 0), 0) : 0; const moneyAtBailiffs = debtCases.length > 0 ? debtCases.reduce((sum, c) => sum + (c.amountAtBailiffs || 0), 0) : 0; const inTransit = totalClaimed > 0 ? totalClaimed - totalRecovered - moneyAtBailiffs : 0; return (
{/* Financial Dashboard for Legal - ENHANCED */}

Возврат задолженности

Прямой приход денежных средств

Взыскано (УК)

{totalRecovered.toLocaleString()} ₽

У приставов

{moneyAtBailiffs.toLocaleString()} ₽

Сумма в работе

{totalClaimed.toLocaleString()} ₽

Прогресс сбора {Math.round(((totalRecovered + moneyAtBailiffs)/totalClaimed)*100)}%

Остаток (Листы/Суд)

{inTransit.toLocaleString()} ₽

{/* View Toggle */}
{viewMode === 'pipeline_board' ? (
{preTrialWorks.map(w => (

{w.debtor?.debtorName || 'Должник'}

{w.debtor?.address}

{(w.debtor?.debt_amount || 0).toLocaleString()} ₽

))}
c.status === 'pre_trial' || c.status === 'litigation').length} color="bg-blue-50 border-blue-200"> {debtCases.filter(c => c.status === 'pre_trial' || c.status === 'litigation').map(c => (
{ setSelectedCase(c); setShowCaseModal(true); }} className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm cursor-pointer hover:border-primary-300">

{c.debtorName}

{c.address}

{c.amount.toLocaleString()} ₽

))}
c.status === 'decision_received').length} color="bg-indigo-50 border-indigo-200"> {debtCases.filter(c => c.status === 'decision_received').map(c => (
{ setSelectedCase(c); setShowCaseModal(true); }} className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm cursor-pointer hover:border-primary-300">

{c.debtorName}

{c.address}

{c.amount.toLocaleString()} ₽

))}
c.status === 'enforcement').length} color="bg-amber-50 border-amber-200"> {debtCases.filter(c => c.status === 'enforcement').map(c => (
{ setSelectedCase(c); setShowBailiffModal(true); }} className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm cursor-pointer hover:border-amber-300">

{c.debtorName}

{c.address}

{(c.amountAtBailiffs || 0).toLocaleString()} ₽ у ФССП

))}
c.status === 'closed').length} color="bg-emerald-50 border-emerald-200"> {debtCases.filter(c => c.status === 'closed').map(c => (
{ setSelectedCase(c); setShowCaseModal(true); }} className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm cursor-pointer hover:border-emerald-300">

{c.debtorName}

{c.address}

Взыскано {(c.recoveredAmount || 0).toLocaleString()} ₽

))}
) : viewMode === 'pipeline' ? (
setSearch(e.target.value)} className="w-full pl-9 pr-4 py-3 bg-white border border-slate-200 rounded-2xl text-sm outline-none focus:ring-2 focus:ring-primary-500 shadow-sm" />
{loading ? (
Загрузка...
) : (
{debtCases.map(caseItem => ( { setSelectedCase(caseItem); setShowCaseModal(true); }} /> ))} {debtCases.length === 0 && (

Дела не найдены

)}
)}
) : (
{/* FSSP Detailed View */}

Зависло у приставов: {moneyAtBailiffs.toLocaleString()} ₽

Деньги взысканы ФССП, но не поступили на счет УК. Требуется сверка с депозитом отдела.

{loading ? (
Загрузка...
) : (
{debtCases.filter(c => c.status === 'enforcement').map(caseItem => ( { setSelectedCase(caseItem); setShowBailiffModal(true); }} /> ))} {debtCases.filter(c => c.status === 'enforcement').length === 0 && (

Нет дел у приставов

)}
)}
)} {/* Case Details Modal */} {showCaseModal && selectedCase && ( { setShowCaseModal(false); setSelectedCase(null); }} onUpdate={(updatedCase) => { setDebtCases(debtCases.map(c => c.id === updatedCase.id ? updatedCase : c)); loadCases(); }} /> )} {/* Bailiff Interaction Modal */} {showBailiffModal && selectedCase && ( { setShowBailiffModal(false); setSelectedCase(null); }} onUpdate={(updatedCase) => { setDebtCases(debtCases.map(c => c.id === updatedCase.id ? updatedCase : c)); loadCases(); }} /> )}
); }; const PipelineColumn: React.FC<{ title: string; count: number; color: string; children: React.ReactNode }> = ({ title, count, color, children }) => (

{title}

{count}
{children}
); const DebtCaseCard: React.FC<{ item: LegalCourtCase; onClick: () => void }> = ({ item, onClick }) => { const isEnforcement = item.status === 'enforcement'; const isLitigation = item.status === 'litigation'; return (
{isEnforcement ? : }
{item.status === 'pre_trial' ? 'Претензия' : item.status === 'litigation' ? 'В суде' : item.status === 'enforcement' ? 'У приставов' : 'Закрыто'} {item.caseNumber}

{item.debtorName}

{item.address}

Сумма долга

{item.amount.toLocaleString()} ₽

{item.recoveredAmount && item.recoveredAmount > 0 && (

{item.recoveredAmount.toLocaleString()} ₽

)}
{isEnforcement ? (

Статус ФССП

{item.fsspStatus}

) : (

Заседание

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

)}
); }; const FSSP_STAGE_ORDER: FsspStage[] = ['writ_submitted', 'ip_initiated', 'bank_requests', 'money_on_deposit', 'transferred_to_uk']; const FSSP_STAGE_LABELS: Record = { writ_submitted: 'ИЛ предъявлен', ip_initiated: 'ИП возбуждено', bank_requests: 'Запросы в банки', money_on_deposit: 'Деньги на депозите', transferred_to_uk: 'Перевод в УК' }; const BailiffActionCard: React.FC<{ item: LegalCourtCase; onClick: () => void }> = ({ item, onClick }) => { const lastAction = item.fsspLastActionDate ? new Date(item.fsspLastActionDate) : new Date(); const daysSinceLastAction = Math.floor((new Date().getTime() - lastAction.getTime()) / (1000 * 3600 * 24)); const isStuck = daysSinceLastAction > 20; const currentStageIndex = item.fsspStage ? FSSP_STAGE_ORDER.indexOf(item.fsspStage) : -1; return (

{item.debtorName}

{item.address}

{item.enforcementNumber && (
ИП: {item.enforcementNumber}
)}
Пристав: {item.bailiffName || '—'}
{daysSinceLastAction} дн. без действий

Сумма у ФССП

{item.amountAtBailiffs?.toLocaleString()} ₽

Из {item.amount.toLocaleString()} ₽ присужденных

Цепочка взыскания:
{FSSP_STAGE_ORDER.map((stage, idx) => { const done = currentStageIndex > idx || (currentStageIndex === idx && stage === 'transferred_to_uk'); const current = currentStageIndex === idx && stage !== 'transferred_to_uk'; return (
); })}
); }; // Bailiff Interaction Modal Component (inline version) interface BailiffInteractionModalComponentProps { courtCase: LegalCourtCase; onClose: () => void; onUpdate: (updatedCase: LegalCourtCase) => void; } const BailiffInteractionModalComponent: React.FC = ({ courtCase, onUpdate, onClose }) => { const [loading, setLoading] = useState(false); const [comments, setComments] = useState([]); const [newComment, setNewComment] = useState(''); const [commentAuthor, setCommentAuthor] = useState(''); const [formData, setFormData] = useState({ fsspStatus: courtCase.fsspStatus || '', bailiffName: courtCase.bailiffName || '', fsspLastActionDate: courtCase.fsspLastActionDate || '', amountAtBailiffs: courtCase.amountAtBailiffs || 0, recoveredAmount: courtCase.recoveredAmount || 0, enforcementNumber: courtCase.enforcementNumber || '', enforcementStartDate: courtCase.enforcementStartDate || '', fsspStage: courtCase.fsspStage || '' }); const [newPetition, setNewPetition] = useState({ type: 'request', text: '', date: new Date().toISOString().split('T')[0] }); useEffect(() => { loadComments(); }, [courtCase.id]); const loadComments = async () => { try { const response = await authFetch(`/api/legal/court-cases/${courtCase.id}/comments`); if (response.ok) { const data = await response.json(); // Фильтруем комментарии, связанные с приставами const bailiffComments = (data || []).filter((c: any) => c.comment.toLowerCase().includes('ходатайство') || c.comment.toLowerCase().includes('пристав') || c.comment.toLowerCase().includes('фссп') ); setComments(bailiffComments); } } catch (error) { console.error('Error loading comments:', error); } }; const handleSave = async () => { setLoading(true); try { const response = await authFetch(`/api/legal/court-cases/${courtCase.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...formData, changedBy: commentAuthor || 'System' }) }); if (response.ok) { const updated = await response.json(); onUpdate(updated); alert('Данные обновлены'); } else { alert('Ошибка при сохранении'); } } catch (error) { console.error('Error saving bailiff data:', error); alert('Ошибка при сохранении'); } finally { setLoading(false); } }; const handleReconciliation = async () => { if (!confirm(`Подтвердите сверку прихода. Сумма ${formData.amountAtBailiffs} ₽ будет перенесена из "У приставов" в "Взыскано".`)) { return; } setLoading(true); try { const newRecoveredAmount = (courtCase.recoveredAmount || 0) + (formData.amountAtBailiffs || 0); const response = await authFetch(`/api/legal/court-cases/${courtCase.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ recoveredAmount: newRecoveredAmount, amountAtBailiffs: 0, changedBy: commentAuthor || 'System' }) }); if (response.ok) { const updated = await response.json(); onUpdate(updated); setFormData({ ...formData, recoveredAmount: newRecoveredAmount, amountAtBailiffs: 0 }); // Добавляем комментарий о сверке if (commentAuthor) { await authFetch(`/api/legal/court-cases/${courtCase.id}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ author: commentAuthor, comment: `Сверка прихода выполнена. Перенесено ${formData.amountAtBailiffs} ₽ из ФССП в взысканные средства.` }) }); await loadComments(); } alert('Сверка прихода выполнена'); } else { alert('Ошибка при сверке прихода'); } } catch (error) { console.error('Error reconciling payment:', error); alert('Ошибка при сверке прихода'); } finally { setLoading(false); } }; const handleAddPetition = async () => { if (!newPetition.text.trim() || !commentAuthor.trim()) { alert('Заполните текст ходатайства и автора'); return; } setLoading(true); try { const response = await authFetch(`/api/legal/court-cases/${courtCase.id}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ author: commentAuthor, comment: `[ХОДАТАЙСТВО] ${newPetition.text}` }) }); if (response.ok) { setNewPetition({ ...newPetition, text: '' }); await loadComments(); alert('Ходатайство добавлено'); } else { alert('Ошибка при добавлении ходатайства'); } } catch (error) { console.error('Error adding petition:', error); alert('Ошибка при добавлении ходатайства'); } finally { setLoading(false); } }; const lastAction = courtCase.fsspLastActionDate ? new Date(courtCase.fsspLastActionDate) : null; const daysSinceLastAction = lastAction ? Math.floor((new Date().getTime() - lastAction.getTime()) / (1000 * 3600 * 24)) : null; return (

Взаимодействие с приставами

{courtCase.caseNumber}

{/* Информация о приставах */}

Информация о приставах

setFormData({ ...formData, enforcementNumber: e.target.value })} placeholder="Номер исполнительного производства" className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500 bg-white" />
setFormData({ ...formData, enforcementStartDate: 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-amber-500 bg-white" />
setFormData({ ...formData, fsspStatus: e.target.value })} placeholder="Например: Деньги на депозите" className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500 bg-white" />
setFormData({ ...formData, bailiffName: e.target.value })} placeholder="Иванов И.И." className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500 bg-white" />
setFormData({ ...formData, fsspLastActionDate: 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-amber-500 bg-white" /> {daysSinceLastAction !== null && (

20 ? 'text-red-600' : 'text-slate-600'}`}> {daysSinceLastAction} дней без действий

)}
{/* Финансовые данные */}

Финансовые данные

setFormData({ ...formData, amountAtBailiffs: 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-amber-500 bg-white" />
setFormData({ ...formData, recoveredAmount: 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-amber-500 bg-white" />
{/* Ходатайства */}

Ходатайства

{comments.filter(c => c.comment.toLowerCase().includes('ходатайство')).length > 0 ? ( comments.filter(c => c.comment.toLowerCase().includes('ходатайство')).map((comment: any) => (

{comment.author}

{new Date(comment.createdAt).toLocaleDateString('ru-RU')}

{comment.comment.replace('[ХОДАТАЙСТВО]', '').trim()}

)) ) : (

Ходатайств пока нет

)}
setCommentAuthor(e.target.value)} placeholder="Ваше имя" className="w-full px-4 py-2 border border-slate-200 rounded-xl text-sm outline-none focus:ring-2 focus:ring-amber-500" />